记一次 _objc_retain 崩溃分析
最近收到 iOS 上一个偶现的 SIGSEGV SEGV_ACCERR
崩溃。错误信息显示该崩溃发生在 _objc_retain
方法,让我困惑了很久。经过分析,发现看似内存问题,实则线程问题。
错误日志
一条典型的错误如下:
1 | 0 libobjc.A.dylib _objc_retain + 16 |
显示错误发生在 AbcLocationRecorder.m
第151行。第151行代码如下:
1 | AbcLocationSample *locationSample = [self prepareDataFromAbcLocation:self.latestLocation regeocode:nil]; |
代码看起来没有任何问题,也很难将其跟 _objc_retain
方法联系起来。
类似案例
处理 iOS 崩溃的经验不多,所以先在网上找了一圈看是否有人遇到类似问题,还果真找到了。
案例一
iOS崩溃分析 - Lightning_S - 博客园 提到了两个崩溃。
一个是 objc_release
,错误如下:
1 | 0 libobjc.A.dylib 0x1b394f150 objc_release + 16 |
另一个是 objc_retain
,错误如下:
文章的结论是:
- 以上崩溃都是对象引用计数变化时没有正确加锁(线程不安全)导致
- 编译器在代码中插入
objc_retainAutoreleasedReturnValue
,所以错误日志中会看到_objc_retain
案例二
从一个crash分析到苹果的代码问题 - 简书 提到属性被声明为 nonatomic
时,当对象在一个线程中释放了,另一个线程访问时就可能出问题。
至于 nonatomic
的线程安全问题,原因如下:
nonatomic取到函数地址后,直接返回指针指向的值,如果这时 *slot 正好被释放,那么返回的就是一个错误的值
而atomic会先retain,然后放到自动释放池,这样就能保证返回的对象一定不会被释放
这里直接贴上相关的代码:
1 | id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { |
问题复现
看过两个类似案例,再来分析自己的问题就有头绪了。出错的第151行代码如下:
1 | // 主线程中读 self.latestLocation |
我们很自然地把怀疑目标锁定在 self.latestLocation
这里。latestLocation
属性定义如下:
1 | @interface AbcLocationRecorder () <AbcLocationManagerDelegate> |
位置更新时通过如下回调来更新 self.latestLocation
:
1 | - (void)mapLocationManager:(AbcLocationManager *)manager didUpdateLocations:(NSArray<AbcLocation *> *)locations { |
总结一下:
- latestLocation 访问修饰符为
nonatomic
- 主线程中读
self.latestLocation
- 子线程中写
self.latestLocation
- 读写
self.latestLocation
没有加锁!
前面提到这个 SIGSEGV SEGV_ACCERR
崩溃在我们 App 中是偶现的,Bugly 上有不少上报,但实际中跟进问题时却一次也没复现。怎么办?
我们写个 demo 吧。demo 如下:
1 | @interface ViewController () |
运行几次很快就会产生崩溃。找到复现方法后,解决问题就很简单了。多线程读写 self.latestLocation
属性中正确地加锁,保证线程安全就能避免崩溃。
更多知识
网上寻找相似案例,是为了快速弄清问题原因。写demo是为了快速找到问题复现方法和解决办法。但我们不能就此止步,因为很多问题还可以更深入。
objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue
编译器在代码中插入
objc_retainAutoreleasedReturnValue
,所以错误日志中会看到_objc_retain
那什么是 objc_autoreleaseReturnValue
呢?
在 MRC 的环境里有以下代码:
1 | // MRC |
我们知道内存分配的原则是”谁分配,谁释放”。这个原则让上述代码很为难,
- 不能在 return 之前释放,因为分配出来的对象还没交给调用方法使用
- 也不可能在 return 之后释放,因为 return 之后作为分配方没法负责释放了(没有对象指针)
所以需要将分配出来的对象注册到自动释放池,也延迟释放
1 | // MRC |
ARC 为我们自动调用了 autorelease
和 retain
两个方法 (自动释放)。考虑到兼容 MRC 时,ARC 自动调用 autorelease
和 retain
两个方法会带来不必要的开销,所以 ARC 使用 objc_autoreleaseReturnValue
和objc_retainAutoreleasedReturnValue
对多余操作优化,以提升性能优化。
How does objc_retainAutoreleasedReturnValue work? - Matt Galloway 中对 objc_retainAutoreleasedReturnValue
有更多解释。这里挑关键点翻译出来。
objc_retainAutoreleasedReturnValue
背后的思路是这样的:如果一个函数返回的对象是 autoreleased 的,并且接下来的对这个对象执行的操作是 retain
,那么这里的 autorelease
和 retain
完全是无意义的,不过是在浪费 CPU 时间。如果某些情况下我们可以检测出这种情况,就能节省CPU时间。在App整个运行期间,节省的CPU时间累计下来就是个不小的优化。
Apple’s code提到:
objc_autoreleaseReturnValue() 检查 return 之后调用方的指令。如果调用方是立即调用 objc_retainAutoreleasedReturnValue,则被调用方不会做 autorelease 操作,而是将结果以 thread-local 方式保存。如果调用方并没有调用 objc_retainAutoreleasedReturnValue,则被调用方会做 autorelease 操作。
objc_retainAutoreleasedReturnValue 会检查返回值是否跟 thread-local 变量中保存的值一致。如果一致,则直接返回结果。如果不一致,则会执行一次 autorelease 和 retain。无论哪种方式,调用方都能拿到一个 retained reference
考虑你有如下代码:
1 | - (SomeClass*)createMeAnObject { |
我们可以重写以上代码,重写后如下:
1 | - (SomeClass*)createMeAnObject { |
如果将 createMeAnObject
内联到 init
,则代码变成这样:
1 | - (id)init { |
注意以上代码中 [temp autorelease]
后紧接着一个 [temp retain]
。这正是新的 Objective-C 运行时可以优化的一个点。
(译者注:有很多细节的分析,这里略过) 编译一个调用 objc_autoreleaseReturnValue()
和 objc_retainAutoreleasedReturnValue()
的方法时,编译器会添加如下指令作为标记。
1 | Thumb mode: |
无论哪种模式,编译器均会添加 mov r7, r7
这条看似无任何意义的指令(它将 r7 寄存器的值 move 到 r7寄存器)。不过这条指令是有意义的,编译器将它作为标识,用于告知 objc_autoreleaseReturnValue
方法:调用方接下来会调用 objc_retainAutoreleasedReturnValue
。
1 | id objc_autoreleaseReturnValue(id object) { |
注:以上是伪代码,x86 版本的代码见这里。
总结一下:ARC 出现之前,我们不得不在代码中写 autorelease 和 retain。ARC 出现之后,虽然不用再写 autorelease 和 retain,但是遗留代码中的 autorelease 和 retain 会导致很多冗余操作,objc_autoreleaseReturnValue
和 objc_retainAutoreleasedReturnValue
正是为了应对这些冗余操作的优化。