Autorelease 机制是 iOS 糟糕的设计?

1,377 阅读3分钟

Autorelease 是 iOS 中苹果提供给开发者用来管理对象内存的工具。其核心价值在于:

  • 利用 Runloop,优化对象释放时性能;
  • 对于方法返回对象,延缓对象释放时机;
  • 对于短时间内大量临时对象,及时释放,减少内存峰值;

那么 Autorelease 带来了哪些问题?

性能和包体

添加进 AutoreleasePool 中的对象可以在 Runloop 空闲时进行释放,一定程度上可以优化程序的性能,但是 Autorelease 机制本身就需要消耗额外的性能。

每个 AutoreleasePoolPage 需要 56byte 来存储 Page 的成员变量:

image.png

(图片引用自: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 大小。 image.png

稳定性

当对使用 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

image.png

产生野指针的原因是 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): image.png

image.png

使用 __attribute__((ns_returns_retained): image.png

image.png

对比可以发现,caller 和 callee 中 mov x29, x29bl _objc_retainAutoreleasedReturnValueb _objc_autoreleaseReturnValue 的汇编都移除了。

批量添加 __attribute__((ns_returns_retained) 也可以当做优化性能和包体的一种手段。