2-3.【OC】【内存管理】autorelease 在 MRC 下到底做了什么?

2 阅读3分钟

在 MRC 下,autorelease 的本质是 “延迟释放” 。它既不是立即销毁对象,也不是增加引用计数,而是将对象的所有权转交给了一个名为 AutoreleasePool(自动释放池) 的管理器。

我们可以通过 “谁来管”“怎么管”“何时管” 三个层面来拆解:


1. 语义层面:一份“死刑缓期执行”名单

当你调用 [obj autorelease] 时,发生了以下逻辑:

  1. 对象的引用计数(retainCount)保持不变
  2. 该对象会被添加进当前线程最顶层的 AutoreleasePool待释放列表中。
  3. 你作为调用者,已经履行了“释放所有权”的义务,你可以放心地把这个对象作为返回值传给别人。

2. 底层物理结构:AutoreleasePoolPage

在 Runtime 层面,AutoreleasePool 并不是一个真正的对象,而是一堆 AutoreleasePoolPage 构成的双向链表

  • 物理布局:每个 Page 占用 4096 字节(虚拟内存的一页)。
  • 存储内容:除了 Page 自身的头信息外,剩下的空间全部用来存储对象的内存地址
  • 入栈操作:调用 autorelease 相当于在 Page 里按顺序插入一个指针。

3. 生命周期:释放的时机

这是开发者最容易产生误解的地方。autorelease 对象到底什么时候死?

情况 A:手动管理的 @autoreleasepool

如果你手动写了 @autoreleasepool { ... }

  • 入口 (Push) :在括号开始处,Runtime 会在 Page 里插入一个 POOL_BOUNDARY(边界哨兵值)。
  • 出口 (Pop) :在括号结束处,Runtime 会从 Page 顶部开始,逐个弹出对象地址,并给它们发送 release 消息,直到遇到最近的那个 POOL_BOUNDARY

情况 B:主运行循环 (Main RunLoop)

如果你没有手动写括号(最常见的情况):

  • iOS 的主线程 RunLoop 在每一轮循环开始时,都会创建一个池。
  • 在循环结束进入休眠前,它会销毁(Pop)这个池。
  • 结论:在主线程,autorelease 对象的生命周期通常持续到当前 Event Loop 结束。

4. 为什么需要它?(解决“返回值的悖论”)

想象一个没有 autorelease 的世界,你要写一个工厂方法:

Objective-C

- (NSString *)getFullName {
    NSString *name = [[NSString alloc] initWithFormat:@"%@ %@", _first, _last];
    // 情况1:我在这里 [name release]; 调用方拿到的是野指针。
    // 情况2:我不写 release; 内存泄露。
    
    // 完美方案:
    return [name autorelease]; 
    // 我不再持有它了,但保证你在当前这一秒内能用它,下一秒由系统回收。
}

5. 性能陷阱:循环中的内存峰值

这是面试中经典的坑。如果你在 for 循环里创建大量 autorelease 对象:

Objective-C

for (int i = 0; i < 1000000; i++) {
    // 假设这个方法返回一个 autorelease 对象
    NSString *str = [NSString stringWithFormat:@"%d", i]; 
}

后果:由于主线程 RunLoop 还没结束,这一百万个对象会一直堆积在内存里,直到循环跑完。这会导致内存瞬间暴涨甚至 OOM。

对策:在循环内部手动增加 @autoreleasepool,强制每一轮循环都清空一次。


总结

特性releaseautorelease
动作立即 1-1标记延后 1-1
管理者当前上下文AutoreleasePool
适用场景确定不再需要时作为方法返回值返回时