动手点关注
干货不迷路
ARC 环境下在多线程中执行赋值代码可能会产生野指针,导致 EXC_BAD_ACCESS 崩溃。
这种崩溃发生的概率很低,在开发和灰度阶段即使执行到相应代码也很难崩溃,因此容易遗漏到正式环境。在上亿级用户的 App 往往会成为 Top 问题,对指标造成影响,并且很难排查。
今日头条在治理 Crash 的过程中彻底解决了数十个此类崩溃,发现其具有一定共性。本文详细分析崩溃发生的过程,以及总结了容易出现问题的场景,希望在大家遇到此类问题时能提供一些思路。
1. 原理
Objective-C 对象的赋值过程包含创建新值、保留旧值、加载新值、释放旧值四步。相比 MRC,ARC 环境中编译器会自动插入保留与释放旧值的步骤:
NSObject *_instance;
void foo(void) {
_instance = [[NSObject alloc] init];
}
这点在 AutomaticReferenceCounting [1] 文档中有提到,通过汇编代码也可以分析:
objc_release 会减小对象的引用计数,减小到 0 时对象就会被销毁,假如这时有其它线程正在使用这个对象,那么使用对象的线程就很可能发生崩溃。
2. 崩溃场景
为了演示仅一行赋值代码就能造成崩溃,以及清晰地分析崩溃的原因,我设计了一个 Demo,在 B 线程中释放 A 线程创建的对象使 C 线程崩溃:
复现过程:
A、B、C 三个线程同时进入
foo
函数A 线程先创建初始值 _instance
A 线程执行到 _instance = x0, 创建了新值并赋给 _instance;此时 _instance 引用计数为 1;
B、C 线程读取到 A 线程创建的初始值 _instance
B、C 线程分别执行到 x1 = _instance 时,从 _instance 中读到线程 A 创建的对象,保存到各自的上下文中;_instance 引用计数仍为 1;
B 线程释放 _instance
B 线程执行
objc_release(x1)
后会释放 _instance;_instance 引用计数变为 0,被销毁;C 线程访问 _instance
C 线程执行到
objc_release(x1)
时访问 _instance;由于 _instance 已经被销毁,访问时会发生崩溃。
使用 lldb 的 thread continue 指令 [2] 来控制整个流程,它可以仅让一个线程执行,其它线程保持挂起。
3 个线程同时进入
foo
函数操作步骤:在
foo
函数里面打上断点,可以多次测试让 3 个线程同时进入断点。如图,线程 2 3 4 同时进入了
foo
函数:线程 2 执行到 _instance = x0,创建初始值并赋给 _instance
操作步骤:在 Thread 2 中给汇编代码第 10 行打断点,执行 thread continue,使 Thread 2 执行完 _instance = x0。
可以看到 Thread 2 创建的实例为 0x000000002813e400: