在 MRC 下,autorelease 的本质是 “延迟释放” 。它既不是立即销毁对象,也不是增加引用计数,而是将对象的所有权转交给了一个名为 AutoreleasePool(自动释放池) 的管理器。
我们可以通过 “谁来管” 、 “怎么管” 、 “何时管” 三个层面来拆解:
1. 语义层面:一份“死刑缓期执行”名单
当你调用 [obj autorelease] 时,发生了以下逻辑:
- 对象的引用计数(retainCount)保持不变。
- 该对象会被添加进当前线程最顶层的
AutoreleasePool的待释放列表中。 - 你作为调用者,已经履行了“释放所有权”的义务,你可以放心地把这个对象作为返回值传给别人。
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,强制每一轮循环都清空一次。
总结
| 特性 | release | autorelease |
|---|---|---|
| 动作 | 立即 | 标记延后 |
| 管理者 | 当前上下文 | AutoreleasePool |
| 适用场景 | 确定不再需要时 | 作为方法返回值返回时 |