14

一次 dispatch_once 引发的 crash 的思考

 4 years ago
source link: https://chipengliu.github.io/2020/10/24/dispatch-once-crash/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

一次 dispatch_once 引发的 crash 的思考

2020-10-242020-10-26iOS

前不久项目上一段代码出现 crash,因为 dispatch_once 出现死锁问题

出现问题的代码简化后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@interface UIDevice (Screen)

- (BOOL)isIPhoneX;

@end

@implementation UIDevice (Screen)

- (BOOL)isIPhoneX {
static BOOL isIPhoneX = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (@available(iOS 11.0, *)) {
UIWindow *mainWindow = [UIApplication sharedApplication].windows.firstObject;
BOOL shouldRemoveWindow = NO;
if (!mainWindow) {
mainWindow = [[UIWindow alloc] init];
mainWindow.backgroundColor = [UIColor clearColor];
shouldRemoveWindow = YES;
}
if (mainWindow.safeAreaInsets.bottom > 0.f) {
isIPhoneX = YES;
}

if (shouldRemoveWindow) {
[mainWindow removeFromSuperview];
}
}
});

return isIPhoneX;
}

@end
1
2
3
4
5
6
7
8
9
10
11
@implementation ViewController

- (BOOL)prefersStatusBarHidden {
if ([[UIDevice currentDevice] isIPhoneX]) {
NSLog(@"do something");
return NO;
}
return YES;
}

@end

UIDevice分类方法 -isIPhoneX 内部调用 window 的 safeAreaInsets 属性,会触发当前显示的 ViewController -prefersStatusBarHidden 方法,而 -prefersStatusBarHidden 内部又再次调用了 -isIPhoneX 从而导致 dispatch_once 递归死锁。

dispatch_once 原理

那么 dispatch_once 为什么会递归使用的时候造成死锁呢?

来看看其源码实现 (这里为了简化代码分析,选用libdispatch-339.1.9,项目上 GCD 版本和此版本不一样,但核心逻辑大致相同)

1
2
3
4
5
6
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
// 1. dispatch_once 逻辑入口
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
struct _dispatch_once_waiter_s {
volatile struct _dispatch_once_waiter_s *volatile dow_next;
_dispatch_thread_semaphore_t dow_sema;
};

#define DISPATCH_ONCE_DONE ((struct _dispatch_once_waiter_s *)~0l)

void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
struct _dispatch_once_waiter_s * volatile *vval =
(struct _dispatch_once_waiter_s**)val;
struct _dispatch_once_waiter_s dow = { NULL, 0 };
struct _dispatch_once_waiter_s *tail, *tmp;
_dispatch_thread_semaphore_t sema;

// 2. 判断 vval == NULL,如果 vval 的初始值为 NULL,返回 YES,同时把 &dow 赋值给 vval;否则返回 NO。
if (dispatch_atomic_cmpxchg(vval, NULL, &dow, acquire)) {
// 2.1. 执行 block
_dispatch_client_callout(ctxt, func);

dispatch_atomic_maximally_synchronizing_barrier();
// above assumed to contain release barrier

// 2.2. dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE) 将 vval 修改为指定状态 DISPATCH_ONCE_DONE
// 表示 block 以及执行完成
tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE, relaxed);

// 2.3. 遍历链表的节点(block 执行过程期间加入的)
// 调用 _dispatch_thread_semaphore_signal 来唤醒等待中的信号量
tail = &dow;
while (tail != tmp) {
while (!tmp->dow_next) {
// 2.4 若 tmp 的 next 指针还没更新完毕,等待其更新完毕
dispatch_hardware_pause();
}
sema = tmp->dow_sema;
tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;

// 2.5 发信号,通知链表其他节点,当前节点的任务以及执行完成
_dispatch_thread_semaphore_signal(sema);
}
} else {
// 3. 若首个任务未执行完毕
dow.dow_sema = _dispatch_get_thread_semaphore();
tmp = *vval;
for (;;) {
// 3.1 若首个任务已经完成,结束等待
if (tmp == DISPATCH_ONCE_DONE) {
break;
}
// 3.2 后续任务加入到链表
// 3.2.1 若 vval != tmp, for 循环等待其他节点插入操作完成
if (dispatch_atomic_cmpxchgvw(vval, tmp, &dow, &tmp, release)) {
// 3.2.2 若 vval == tmp,vval = &dow,更新队列节点为当前节点
dow.dow_next = tmp;
// 3.3 阻塞当前线程
_dispatch_thread_semaphore_wait(dow.dow_sema);
break;
}
}
_dispatch_put_thread_semaphore(dow.dow_sema);
}
}
image-20201025190248972

- (BOOL)isIPhoneX 方法中 dispatch_once 传入的 block 内部在相同线程递归执行 - (BOOL)isIPhoneX ,导致前一个任务被后一个任务阻塞,后一个任务又依赖于前一个的完成状态,导致死锁

  1. 上文的 crash 是从逻辑细节看,因为传入 dispatch_once 的 block 内部递归执行触发同一个 dispatch_once。而从函数设计来看,是因为函数内部逻辑不够单纯,出现了外部依赖。dispatch_once 执行的 block 应该尽可能保持单纯、简单。

  2. 对于 - (BOOL)isIPhoneX 这个case 来说,允许极端情况下执行多次内部判断逻辑对性能影响不大,退而求之,可使用静态全局变量保存执行记录;如果遇到极端场景,一开始是并发执行,就允许多次执行完整的判断逻辑,而之后直接使用计算过的结果。这里可以参考QMUI 中判断全面屏的逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    static NSInteger isNotchedScreen = -1;
    + (BOOL)isNotchedScreen {
    if (@available(iOS 11, *)) {
    if (isNotchedScreen < 0) {
    if (@available(iOS 12.0, *)) {
    SEL peripheryInsetsSelector = NSSelectorFromString([NSString stringWithFormat:@"_%@%@", @"periphery", @"Insets"]);
    UIEdgeInsets peripheryInsets = UIEdgeInsetsZero;
    [[UIScreen mainScreen] qmui_performSelector:peripheryInsetsSelector withPrimitiveReturnValue:&peripheryInsets];
    if (peripheryInsets.bottom <= 0) {
    UIWindow *window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
    peripheryInsets = window.safeAreaInsets;
    if (peripheryInsets.bottom <= 0) {
    UIViewController *viewController = [UIViewController new];
    window.rootViewController = viewController;
    if (CGRectGetMinY(viewController.view.frame) > 20) {
    peripheryInsets.bottom = 1;
    }
    }
    }
    isNotchedScreen = peripheryInsets.bottom > 0 ? 1 : 0;
    } else {
    isNotchedScreen = [QMUIHelper is58InchScreen] ? 1 : 0;
    }
    }
    } else {
    isNotchedScreen = 0;
    }

    return isNotchedScreen > 0;
    }

5.44 Built-in functions for atomic memory access

滥用单例之dispatch_once死锁


Recommend

  • 108
    • 微信 mp.weixin.qq.com 7 years ago
    • Cache

    一个 Bug 引发的思考(超赞的文章)

    原文作者:董庆明原文地址:https://zhuanlan.zhihu.com/p/30576566特别声明:本文为董庆明原创并授权发布,未经原作者允许请勿转载,转载请联系原作者

  • 110
    • 掘金 juejin.im 7 years ago
    • Cache

    一个小需求引发的思考

    在平时开发过程中难免为了赶进度或者在比较短的时间里写一个功能,我们一般都简单粗暴的以解决问题为目的,我想对于这样的代码,而后再细细思考才是,没准会有新的发现,今天我就遇到了这么一个小需求。 需求如下:如下图,有两个输入框,一个按钮,需求是当两个Ed...

  • 73
    • GAD腾讯游戏开发者平台 gad.qq.com 7 years ago
    • Cache

    &ldquo;吃鸡&rdquo;热潮引发的思考:这一波热潮,没有赢家。

    2017年,在我看来,是游戏史中可以被浓墨重彩留下一笔的一年。不仅大作频发,一些事件也引发高度热议。记录一下我目前想到的一些大事:1. “吃鸡”热潮,热度甚至超越第一电竞《英雄联盟》;2.&

  • 60
    • 掘金 juejin.im 6 years ago
    • Cache

    首页白屏的引发的思考(一)

    最近在做项目的优化,除了整体的架构更改,我们发现在每次加载的时候,首页白屏的问题十分明显。 为什么会出现白屏 现在的前端框架, React、Vue、Angular 三大巨头已经占据了主导地位,市面上大多数前端应用也都是基于这三个框架或库完成,这三个框架有一个

  • 49
    • 微信 mp.weixin.qq.com 6 years ago
    • Cache

    海量数据处理——从Top K引发的思考

  • 54
    • 微信 mp.weixin.qq.com 6 years ago
    • Cache

    海量数据处理:从 Top K 引发的思考

    (题图:from  github) 三问海量数据处理: 什么是海量数据处理,为什么出现这种需求? 如何...

  • 9

    Secrets of dispatch_oncemikeash.com: just this guy, you know? Friday Q&A 2014-06-06: Secrets of dispatch_once by Mike Ash   Reader Paul Kim po...

  • 4

    iOS疑难问题排查之深入探究dispatch_group crash 昨天其他部门的同事突然反馈一起相对来说比较严重的Crash问题(占比达到了yyyy左右,并且从Crash堆栈上可以发现很多情况下是一启动就Crash了)。去掉隐私数据大致堆栈如下:...

  • 9
    • satanwoo.github.io 4 years ago
    • Cache

    滥用单例之dispatch_once死锁

    滥用单例之dispatch_once死锁 上周排查了一个bug,现象很简单,就是个Crash问题。但是读了一下crash Log以后,却发现堆栈报的错误信息却是第一次见到(吹牛的说,我在国内的iOS也能算第十二人了),包含以下还...

  • 6
    • www.androidchina.net 3 years ago
    • Cache

    一张图引发的App crash

    一张图引发的App crash – Android开发中文站 你的位置:Android开发中文站 >

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK