Autorelease 是 iOS 中苹果提供给开发者用来管理对象内存的工具。其核心价值在于:
- 利用 Runloop,优化对象释放时性能;
- 对于方法返回对象,延缓对象释放时机;
- 对于短时间内大量临时对象,及时释放,减少内存峰值;
那么 Autorelease 带来了哪些问题?
性能和包体
添加进 AutoreleasePool 中的对象可以在 Runloop 空闲时进行释放,一定程度上可以优化程序的性能,但是 Autorelease 机制本身就需要消耗额外的性能。
每个 AutoreleasePoolPage 需要 56byte 来存储 Page 的成员变量:
(图片引用自:draveness.me/autorelease…
对于对象使用非 new/alloc/copy/mutableCopy 开头的方法创建时,编译器需要额外在 caller 中添加汇编指令:
mov x29, x29
bl _objc_retainAutoreleasedReturnValue
在 callee 中添加汇编指令:
b _objc_autoreleaseReturnValue
用于判断是否需要将该对象添加进 AutoreleasePool 中,这些处理最终会导致二进制体积增大以及性能降低。
苹果也意识到了上述问题,在 WWDC2022 Improve app size and runtime performance 中介绍了优化方案:
通过比较指针替代 mov 指令打标,从而可以减少 4byte 大小。
稳定性
当对使用 new/alloc/copy/mutableCopy 开头的方法进行 hook 时,如果不调用原方法,这时就需要对新方法名进行 new/alloc/copy/mutableCopy 约束,否则就会产生野指针问题:
// 原方法
+ (NSObject *)newObject
{
NSObject *obj = [[NSObject alloc] init];
return obj;
}
// 汇编
// 不含 objc_autoreleaseReturnValue 函数调用
+[TestObject newObject]:
-> 0x1043a846c <+0>: adrp x8, 9
0x1043a8470 <+4>: ldr x0, [x8, #0x2e8]
0x1043a8474 <+8>: b 0x1043a99e4 ; symbol stub for: objc_alloc_init
// hook 后的方法
+ (NSObject *)test_newObject
{
NSObject *obj = [[NSObject alloc] init];
return obj;
}
// 汇编
// 含 objc_autoreleaseReturnValue 函数调用
+[ViewController test_newObject]:
0x104a74288 <+0>: stp x29, x30, [sp, #-0x10]!
0x104a7428c <+4>: mov x29, sp
-> 0x104a74290 <+8>: adrp x8, 9
0x104a74294 <+12>: ldr x0, [x8, #0x2c0]
0x104a74298 <+16>: bl 0x104a759fc ; symbol stub for: objc_alloc_init
0x104a7429c <+20>: ldp x29, x30, [sp], #0x10
0x104a742a0 <+24>: b 0x104a75a20 ; symbol stub for: objc_autoreleaseReturnValue
产生野指针的原因是 hook 后的 test_newObject 返回的对象被添加进了 AutoreleasePool 中,导致释放两次。
这里带来的另一个问题就是崩溃问题的归因,我们经常能够在线上监控到 AutoreleasePool 相关的野指针问题,类似上图堆栈基本看不到的业务堆栈信息,这就给问题的排查增加了极大的难度,很多历史 Top 崩溃问题都是因为 AutoreleasePool 释放对象时堆栈信息不足导致。
虽然通过 zombie 监控可以获取到对象的类型以及首次 dealloc 时的堆栈,一定程度可以缓解完全没有有效堆栈信息的问题,但如果首次 dealloc 也发生在 AutoreleasePool 中,那么问题就会非常棘手。
因 Autorelease 导致的疑难问题还可以阅读: 一段防护代码引发的内存风暴。
禁用 Autorelease
我们知道使用 new/alloc/copy/mutableCopy 开头的方法,编译器不会自动插入 Autolrease 相关代码。背后的逻辑是因为编译器提供了 __attribute__((ns_returns_retained)),new/alloc/copy/mutableCopy 会默认使用 ns_returns_retained 属性。
不使用 __attribute__((ns_returns_retained):
使用 __attribute__((ns_returns_retained):
对比可以发现,caller 和 callee 中 mov x29, x29、bl _objc_retainAutoreleasedReturnValue、b _objc_autoreleaseReturnValue 的汇编都移除了。
批量添加 __attribute__((ns_returns_retained) 也可以当做优化性能和包体的一种手段。