1.缘由

最近在修复项目中的遗漏的Crash问题时,发现一个比较隐晦的队列同步死锁问题,记录了下整个发现过程。

2.经过

首先查看友盟统计上的Crash

发现基本都是signal问题,一开始以为是内存泄漏引起的野指针,解决了大部分内存泄漏后,发现Crash问题并没有改善,于是继续排查。

选择一个后查看堆栈:(对项目名相关做了些涂抹处理)

通过dSYM文件,对堆栈进行符号化:

发现是[FMDatabaseQueue  inDatabase]后抛出异常了,于是开始在FMDB的github上查找类型问题,在issues上搜索发现了类似的奔溃,https://github.com/ccgus/fmdb/issues/613,里面有人解答说的是 inDatabase方法嵌套使用造成死锁了,查看[FMDatabaseQueue  inDatabase]源码,内部是一个同步queue的操作:

在同步操作前FMDB也有相应的断言判断代码当前是否在同一个queue:

看来确定是这个导致的Crash,仔细查看业务逻辑代码后,发现业务代码中并没有比较明显的[FMDatabaseQueue  inDatabase]嵌套调用,到底是怎么出现的呢?于是重新查看Crash堆栈,发现两次[FMDatabaseQueue  inDatabase]中间有一段NSNotification的转发:

于是开始写伪代码复现,逻辑如下:

可以看到确实Crash了,奔溃的堆栈和友盟统计上收集到的堆栈基本类似,可以确定就是这个问题了。

3.分析

大致问题是因为串行队列同步嵌套问题,这里虽然创建了一个串行队列_queue,但是由于是在主线程中进行dispatch_sync操作,

dispatch_sync(_queue, ^{
    [self postNotification:@"TestNotification"];
});

- (void)postNotification:(NSString *)notification的实现是把通知处理函数转发到主线程的封装,在这里实际上[self postNotification:@"TestNotification"]是在主线程中运行,当不是在主队列,而内部使用[NSThread isMainThread]来判断是否异步到Main队列的,且- (void)handleNotification:(NSNotification *)notification所在的线程和post时候是一致的,从而导致了嵌套同步队列。
简化去掉Notification相关代码后等同于:

dispatch_sync(_queue, ^{
    dispatch_sync(_queue, ^{
        NSLog(@"");
    });
});

这样就造成_queue同步死锁了。

正确做法应是把[NSThread isMainThread]改为判断是否主队列

这样由于中间经过了async到主队列,后续就算再sync到目标队列也能避免这个问题。

4.总结

这个问题总共有几个知识点:

1.NSNotification的处理函数所在的线程与Post Notification时的线程一致,官方文档也有相关说明。

2.dispatch_sync后代码块所在的线程和进入dispatch_sync时的线程一致。

3.dispatch_get_main_queue()所在线程一定在MainThread,但MainThread所在队列不一定在dispatch_get_main_queue()

其实这个问题只是个比较常见的队列同步死锁问题,但因为中间经过Notification转发,导致代码不好CodeReview,从而在开发阶段没发现问题,这次分析总结主要也是希望在类似问题上能给大家提供到一些帮助。