在 ARC 环境下,循环中内存暴涨的根本原因在于:自动释放对象的“死亡时间”被推迟到了当前 RunLoop 的末尾。
虽然 ARC 帮我们省去了手写 release 的麻烦,但它依然遵循 Objective-C 的 Autorelease 机制。
1. 临时对象的“暂存”逻辑
在循环中,很多类方法(如 [NSString stringWithFormat:]、[UIImage imageNamed:] 或从模型转换出的临时对象)返回的是 Autorelease 对象。
- 编译器行为:ARC 会在底层将这些对象标记为
autorelease。 - 存放位置:这些对象被放进了当前线程的
AutoreleasePool栈中。 - 释放时机:只有当当前的
AutoreleasePool被销毁(Pop)时,里面的对象才会收到release消息并真正释放内存。
2. RunLoop 的“懒清理”
主线程的 AutoreleasePool 是由 RunLoop 管理的。
- RunLoop 每一轮循环开始时创建一个池,结束休眠前才销毁池。
- 问题所在:如果你在一个 RunLoop 循环内(比如一个点击事件的响应方法里)写了一个执行 100 万次的
for循环,那么这 100 万个临时对象都会堆积在内存里,直到你的for循环跑完、函数返回、RunLoop 到达终点。
3. 内存暴涨的物理表现
- 对象地址堆积:
AutoreleasePoolPage会不断开辟新的 4KB 页面来存储这些对象的指针。 - 引用计数未归零:虽然你已经不再使用这些变量,但因为它们在“池子”里被强引用着,内存无法回收。
- 峰值压力:这种现象被称为“内存峰值(Memory Spike)”。如果对象较大(如图片处理),程序会迅速触发 OOM (Out Of Memory) 崩溃。
4. 解决方案:手动干预生命周期
解决办法非常简单:在循环内部手动添加 @autoreleasepool {}。
Objective-C
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
// 产生大量临时对象的逻辑
NSString *str = [NSString stringWithFormat:@"Batch-%d", i];
UIImage *image = [self processImageAtIndex:i];
// ... 使用完后 ...
} // 括号结束的一瞬间,池子立即 Pop,本轮循环产生的对象被立即释放
}
为什么有效? 通过手动加池,你把这些对象的生命周期从“随 RunLoop 销毁”缩短到了“随单次循环销毁”。每执行完一次循环,内存就会回落到正常水平,曲线从“陡坡型”变成了“锯齿型”。
5. 一个特例:ARC 的优化
并不是所有循环都会暴涨。
- 如果你在循环里使用
[[NSObject alloc] init]获取对象,且在循环末尾不再引用它,ARC 可能会直接插入objc_release而不走autorelease。 - 暴涨通常只发生在: 调用了返回
autorelease对象的类方法,或者复杂的闭包(Block)捕获场景中。