起因

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

Thread 0 Crashed:
0   libdispatch.dylib               0x000000018953e828 _dispatch_group_leave :76 (in libdispatch.dylib)
1   libdispatch.dylib               0x000000018954b084 __dispatch_barrier_sync_f_slow_invoke :320 (in libdispatch.dylib)
2   libdispatch.dylib               0x000000018953a1bc __dispatch_client_callout :16 (in libdispatch.dylib)
3   libdispatch.dylib               0x000000018953ed68 __dispatch_main_queue_callback_4CF :1000 (in libdispatch.dylib)
4   CoreFoundation                  0x000000018a65e810 ___CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ :12 (in CoreFoundation)
5   CoreFoundation                  0x000000018a65c3fc ___CFRunLoopRun :1660 (in CoreFoundation)
6   CoreFoundation                  0x000000018a58a2b8 _CFRunLoopRunSpecific :444 (in CoreFoundation)
7   GraphicsServices                0x000000018c03e198 _GSEventRunModal :180 (in GraphicsServices)
8   UIKit                           0x00000001905d17fc -[UIApplication _run] :684 (in UIKit)
9   UIKit                           0x00000001905cc534 _UIApplicationMain :208 (in UIKit)
10  xxxiPhone                       0x0000000100041a98 main main.m:26 (in xxxiPhone)
11  libdyld.dylib                   0x000000018956d5b8 _start :4 (in libdyld.dylib)

一看到这种堆栈,头就大了,除了Thread 0 的第10行是和程序本身二进制相关的堆栈,其余的调用栈全部是系统库里面的,并且唯一一行程序本身二进制的代码还是一个完全没作用的main函数。

好吧,只能重新找找其余的线索。从堆栈上来反推当时的场景应该是如下场景:

启动 -> main函数 -> main_queue 执行 -> dispatch_group_leave -> Crash

于是,我们的线索就从最后的_dispatch_group_leave来进行。

首先先来最简单的方法:下符号断点:dispatch_group_leave

当然事情没有这么简单,尝试重复多次也没有断到我们想要的符号断点上,于是这条路暂时考虑放弃(结合Crash率也可以发现这并非必现的Crash场景)。

这条路不通,我们先尝试全局搜索dispatch_group_leave,结果发现有如下几条线索:

  • 外部开源库
  • 自身工程代码

结合Crash出现的版本以及以上上述各库最后升级时间来判断,我们基本确定出在问题出现在自身工程中的代码里,如下:

dispatch_group_t serviceGroup = dispatch_group_create();
dispatch_group_notify(serviceGroup, dispatch_get_main_queue(), ^{NSLog(@"ttttttt:%@",t);
});// t 是一个包含一堆字符串的数组
[t enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {dispatch_group_enter(serviceGroup);SDWebImageCompletionWithFinishedBlock completion =^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {dispatch_group_leave(serviceGroup);NSLog(@"idx:%zd",idx);};[[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:t[idx]]options:SDWebImageLowPriorityprogress:nilcompleted:completion];
}];

这段代码逻辑非常简单吧:给你一个数组,里面是一堆图片地址。你使用多线程进行并发下载,直到所有图片都下载完成(可以失败)进行回调,其中图片下载使用的是SDWebImage

这段代码里面的的确确出现了可疑的dispatch_group_leave,但是这段代码太常见了。和同事认认真真检查了许久,同时也和天猫、手淘中使用dispatch_group_t的地方进行了对比,没发现任何问题。

好吧,问题一下子陷入了僵局,只好上终极调试大法:汇编分析法

通过文章开头的堆栈我们查找libdispatch.dylib中对应的Crash位置,然后通过汇编解析查看相关指令,结果如下:

从上图看出,指令挂掉的原因是因为执行了brk (brk可以理解为跳转指令特殊的一种,一旦执行,就会进入某种Exception模式,导致Crash)。

为什么执行dispatch_group_leave会挂?从上述图中汇编不难发现,dispatch_group_leave具有两条分支:比较x9寄存器和0之间的关系,如果是less equal,就跳转到0x180502808(即会crash的逻辑分支);反之则正确执行ret返回。

那么x9寄存器是什么?我们继续往上看指令ldxr x9, [x10],x9中的值是以x10寄存器中的内容作为地址,取64位放入x9寄存器中。继续,那么x10中的内存是什么?x10中的内容是指令add x10, x0, #0x30。也就是x10 = x0 + 48(0x30的10进制表示)。那么,函数调用的时候x0是self,也即是一个类或者结构体的首地址。所以这两句指令加起来的含义就是取结构体地址偏移48位置的某个成员变量的值。

除此之外,汇编解析还完整保留了Crash的字符串提示: “BUG IN CLIENT OF LIBDISPATCH: Unbalanced call to dispatch_group_leave()”

结合这两点,我们查看libdispatch的源码,代码如下:

void
dispatch_group_leave(dispatch_group_t dg)
{dispatch_semaphore_t dsema = (dispatch_semaphore_t)dg;dispatch_atomic_release_barrier();long value = dispatch_atomic_inc2o(dsema, dsema_value);if (slowpath(value == LONG_MIN)) {DISPATCH_CLIENT_CRASH("Unbalanced call to dispatch_group_leave()");}if (slowpath(value == dsema->dsema_orig)) {(void)_dispatch_group_wake(dsema);}
}

注:苹果开发的libdispatch源码经过了各种变形修改,不是真正运行的代码,仅供参考。

果不其然,这段代码完整复现了我们之前汇编分析的结果:如果dg信号量中的字段dsema_value原子性自加一后等于LONGMIN,就会CRASH。为什么会Crash呢?

我们需要关注下LONG_MIN这个数字,LONG_MIN = -LONG_MAX - 1。理解起来很简单,就是可以表征的(该类型合法范围)最大数和最小数。

搜索下LONGMAX,我们发现在dispatch_group_create里面发现了它的踪影:

dispatch_group_t
dispatch_group_create(void)
{dispatch_group_t dg = _dispatch_alloc(DISPATCH_VTABLE(group),sizeof(struct dispatch_semaphore_s));_dispatch_semaphore_init(LONG_MAX, dg);return dg;
}

好了, 这下豁然开朗。这两段代码的结合告诉了我们一个事实:当dq这个信号量加一导致溢出后,dispatch_group_leave就会Crash。

最简单的复现代码如下:

- (void)viewDidLoad
{[super viewDidLoad];dispatch_group_t group = dispatch_group_create();dispatch_group_leave(group);// Do any additional setup after loading the view, typically from a nib.
}

当然,上述代码相当直白简单,我们一般都不会犯这样低级的错误。

代码究竟出错在哪?

了解了dispatch_group_leave的出错原因后,我们再回到我们刚刚认为没问题的代码,一定是哪个地方我们欠考虑了。

上述代码执行流程还是非常简单的,我们用模型简述一遍:

遍历数组,对每个URL进行dispatch_group_enter,然后将其丢入一个下载block交由SDWebImage进行并发下载,下载回调(无论失败或者成功)后执行dispatch_group_leave

我们举个简单的例子,假设我们有一个包含5个URL的数组:

  1. 遍历的时候,对信号量dq enter了5次,简单理解信号量减去5次。
  2. SDWebImage下载回调的时候,对信号量dq leave了5次,于是信号量增加了5次。
  3. 执行完毕,整个group执行完成。

但是,由于SDWebImage的下载是异步且无法保证时间的,如果在整个group没有执行完毕期间,上述函数整体又被执行到了,会怎么样?

我们再用上述的例子来走遍流程。

  1. 第一次遍历,我们创建了信号量dq1,enter了5次,dq1 现在 = -5。
  2. SDWebImage的下载回调捕捉了dq1,准备留待回调后加回来,我们将这次遍历生成的下载回调block统称为b10, b12, b13, b14, b15。
  3. 但是,在第一次SDWebImage下载回调还没执行的时候,第二次函数遍历来了。
  4. 第二次遍历,我们创建了信号量dq2,enter了5次,dq2 现在 = -5。
  5. 创建第二次遍历对应的回调block,称为b20,b21, b22, b23, b24。

通过查阅SDWebImageDownloader.m源码我们发现:

dispatch_barrier_sync(self.barrierQueue, ^{SDWebImageDownloaderOperation *operation = self.URLOperations[url];if (!operation) {operation = createCallback();// !!!!!!!特别注意这行!!!!!!!!!self.URLOperations[url] = operation;__weak SDWebImageDownloaderOperation *woperation = operation;operation.completionBlock = ^{SDWebImageDownloaderOperation *soperation = woperation;if (!soperation) return;if (self.URLOperations[url] == soperation) {[self.URLOperations removeObjectForKey:url];};};
}

SDWebImage的下载器会根据URL做下载任务对应NSOperation映射,也即之前创建的下载回调Block。

好,就是这行导致Crash的发生。为什么呢?

我们设想下,假设在第二次遍历中包含了第一次遍历中的图片URL,比如b20对应的图片URL和b10对应的图片URL一样,那么在SDWebImage的处理回调里,b20就会替换掉b10。于是,在第一次遍历创建的5个下载任务回调中,b10回调的时候实际已经执行的是b20,也就是dq2 + 1;而在后续第二次遍历执行下载任务回调的时候,又分别执行了b20-b24的5个任务,导致dq2 + 5。这从导致dq2实际上leave的次数比enter的次数多了1 (6比5),导致了dq2信号量的数值溢出,从而进入了Crash分支。

最后

看起来很简单、清晰易懂的代码,没想到也会造成巨大的问题。所以,写代码一定要谨慎谨慎再谨慎。

iOS疑难问题排查之深入探究dispatch_group crash相关推荐

  1. iOS拓展---【转载】iOS客户端节日换肤方案探究

    [转载]iOS客户端节日换肤方案探究 一.前言: Tip: 本来这篇文章在圣诞节就已经准备好了,但是由于种种原因一直没有写完,今天将它写出来,也算是2018年的第一篇文章了.你好,2018! 过去圣诞 ...

  2. iOS中注册功能的体验探究

    登录功能是我在湖畔做的第一个需求. 当时PD给我的草图和下图类似: (图片来自知乎iOS客户端登录界面) 不过需求中要求用户名或者密码错误时,输入框要抖动(类似Mac登录密码错误的抖动效果). 如果实 ...

  3. iOS 内存泄漏排查方法及原因分析

    级别: ★★☆☆☆ 标签:「iOS」「内存泄漏排查」「Leaks工具」 作者: MrLiuQ 审校: QiShare团队 本文将从以下两个层面解决iOS内存泄漏问题: 内存泄漏排查方法(工具) 内存泄 ...

  4. iOS 视图,动画渲染机制探究

    腾讯Bugly特约作者:陈向文 终端的开发,首当其冲的就是视图.动画的渲染,切换等等.用户使用 App 时最直接的体验就是这个界面好不好看,动画炫不炫,滑动流不流畅.UI就是 App 的门面,它的体验 ...

  5. iOS客户端节日换肤方案探究

    转自:https://www.ianisme.com的博客 一.前言: tip: 本来这篇文章在圣诞节就已经准备好了,但是由于种种原因一直没有写完,今天将它写出来,也算是2018年的第一篇文章了.你好 ...

  6. ios开发遇到的memory持续上涨导致页面crash解决思路总结

    我在IOS遇到过的闪退主要分为程序启动完Lanch page在初始化页面就崩溃,和在程序运行中crash两种: 后者我遇到的情况是memory占用过多,被系统kill掉了一部分正在占用的内存,导致程序 ...

  7. ios nstimer实现延时_iOS 中常见 Crash 总结

    作者 | 在路上重名了啊 @(iOS总结)[温故而知新] [TOC] 1.找不到方法的实现unrecognized selector sent to instance 2.KVC造成的crash 3. ...

  8. ios无痕埋点_iOS无痕埋点方案分享探究

    原标题:iOS无痕埋点方案分享探究 作者丨SandyLoo https://www.jianshu.com/p/b8a67c4acfb3 前言 当前互联网行业的竞争已经是非常激烈了, "功能 ...

  9. UE 手游在 iOS 平台运行时内存占用太高?试试这样着手优化

    性能优化,对游戏开发来说是一个需要不断钻研的课题,性能越好,游戏才会运行的更加顺畅,玩家的体验感才会更好.腾讯游戏学院专家.游戏客户端开发 Leonn,将和大家分享 UE 手游在 iOS 平台上的内存 ...

最新文章

  1. 凸函数和非凸函数---and why
  2. websocket中发生数据丢失_为什么事实上却发生了数据丢失,只有少部 分数据可以加载进来...
  3. .Net装箱拆箱编程实例
  4. 【ubuntu】解决窗口管理器 不支持透明问题(11.04之前版本不支持)
  5. 微x怎么设置主题_红人堂:抖音直播预告文案怎么写?5个小技巧提高你的文案吸引力!...
  6. lightgbm的GPU版本和CPU版本运行速度比较
  7. 微软发布 VS Code 容器化开发工具,大大简化物联网设备开发
  8. mac安装gdb及为gdb进行代码签名
  9. 如何用AnySDK快速接入SDK上线
  10. 中国碳酸镁铝行业市场供需与战略研究报告
  11. 商务口语:议价时可能用到的句子
  12. 如何自制一款智能AI离线语音小夜灯
  13. SPSS软件数据中心化、标准化和归一化
  14. deepin linux live cd,Deepin Live cd修复引导
  15. 搜狗AI事业部张博:不只翻译机,半年内将推数款智能硬件产品
  16. 【mysql的日期和时间类型】
  17. docker 安装clickhouse(springboot mybatisplus clickhouse 整合)
  18. linux无线8179,编译安装0bda 8179无线网卡
  19. #### Kafka Rebalance ####
  20. js实现网页中英文翻译

热门文章

  1. MyEclipse一定要做的事-改变默认编码
  2. putty 32位_了解linux系统远程操作软件,putty的安装过程!
  3. docker删除镜像、容器命令
  4. Mysql替换字段中的内容
  5. Kotlin入门(20)几种常见的对话框
  6. Kotlin入门(1)搭建Kotlin开发环境
  7. 记录一次和朋友聊天遇到的面试题 ip地址字符串和long类型的相互转换 都是参考了别人的代码 加了一些个人理解的总结
  8. 12c rman中输入sql命令
  9. Citrix小贴纸---连接XenAPP时协议驱动程序错误
  10. SQL SERVER 2000安装教程图文详解