2-12.【OC】【内存管理】ARC 下为什么循环里创建大量临时对象容易内存暴涨?

1 阅读2分钟

在 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. 内存暴涨的物理表现

  1. 对象地址堆积AutoreleasePoolPage 会不断开辟新的 4KB 页面来存储这些对象的指针。
  2. 引用计数未归零:虽然你已经不再使用这些变量,但因为它们在“池子”里被强引用着,内存无法回收。
  3. 峰值压力:这种现象被称为“内存峰值(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)捕获场景中。