Swift 与 Objective-C

@autoreleasepool 完整解析(解决循环中的内存暴增)

“都已经是 ARC 时代了,为什么还要使用 @autoreleasepool?”

4 分钟阅读
@autoreleasepool 完整解析(解决循环中的内存暴增) 封面图

“都已经是 ARC 时代了,为什么还要使用 @autoreleasepool?”

这是我经常被后辈问到的问题。明明是在某处见过的关键字,但要立即回答什么时候以及为什么使用它,却意外地不容易。

今天就从@autoreleasepool是什么开始,讲清楚实际开发中什么时候该使用它。

先说结论。

@autoreleasepool是一个代码块,可以在你指定的时间,提前释放那些“预约稍后释放的对象”。在循环中大量产生临时对象时,防止内存暴增是它最典型的用途。

下面逐一说明。


什么是自动释放池?

先从背景概念开始。

Objective-C 很早以前就提供了autorelease这一机制。它会把对象预约为“现在先不要,过一会儿再 release”。

这些已预约对象排队等待的列表,就是 autorelease pool。

当 pool 被 drain 时,列表中的对象会一次性收到 release。

那么,这个 pool 什么时候会被清空呢?

在 iOS App 中,主运行循环会自动管理它。每次完成一个周期——处理触摸事件,再刷新画面——都会彻底清空 pool。

所以平时我们无需操心。


问题会在循环中爆发

不过,“每次周期结束时”这个条件,在某些情况下反而会成为问题。

也就是在一次运行循环执行期间,积累了大量临时对象。

例如,假设在一个循环中处理数千张照片。

for (int i = 0; i < 5000; i++) {
    // 每次迭代都会临时创建一个大型图像对象
    UIImage *image = [self loadAndResizeImage:i];
    [self saveThumbnail:image];
}

循环运行期间,运行循环没有机会清空 pool。

因此,仅被标记为等待释放的临时对象,会完整积累相当于 5000 张照片的内存。

内存图像像山一样飙升,严重时系统会强制终止 App。

循环运行期间内存图像像山一样飙升的瞬间
循环运行期间内存图像像山一样飙升的瞬间

用 @autoreleasepool 削平这座山

解决方法很简单:在循环内部创建专用的 pool,每次迭代后手动清空。

for (int i = 0; i < 5000; i++) {
    @autoreleasepool {
        UIImage *image = [self loadAndResizeImage:i];
        [self saveThumbnail:image];
    } // 代码块结束的瞬间,临时对象就会立即释放
}

@autoreleasepool代码块关闭时,其中创建的临时对象会立即清理。

不再是积累相当于 5000 张照片的内存后一次性释放,而是每次积累一张、释放一张。

原本山形的内存图像会变成平缓的锯齿形。


在 Swift 中这样使用

你可能会说:“我只用 Swift。”Swift 也有完全相同的工具。

就是autoreleasepool函数。

for i in 0..<5000 {
    autoreleasepool {
        let image = loadAndResizeImage(i)
        saveThumbnail(image)
    }
}

有一点需要注意。

纯 Swift 对象大多不会经过 autorelease pool,作用域结束时就会立即释放。

这个工具真正发挥作用的场景,是重复调用UIImage、Data(contentsOf:)、NSData等基于 Objective-C 的 Foundation・UIKit API。

因为现在仍有许多 API 会在内部创建并返回 autorelease 对象。

即使在 Swift 中,一个 autoreleasepool 代码块也能让图像变得平稳
即使在 Swift 中,一个 autoreleasepool 代码块也能让图像变得平稳

这些时候就该使用

总结来说,需要使用@autoreleasepool的情况包括以下几种。

  • 在循环中大量创建图像、文件、字符串等大型临时对象时
  • 在后台线程中没有运行循环,却需要长时间执行任务时
  • 在命令行工具等没有运行循环的环境中使用 Objective-C 对象时

反过来,普通 UI 代码和一般业务逻辑通常不需要特意使用。主运行循环已经管理得很好。

也不建议不做测量就习惯性地包裹代码。应先在 Instruments 或 Xcode 内存图像中确认实际出现峰值,再决定使用。


常见问题(Q&A)

Q. ARC 会自动处理,为什么还要我管理 pool?

ARC(Automatic Reference Counting,自动引用计数)只会替你插入 retain 和 release,并不会改变 autorelease pool 何时清空。提前清空 pool 的“时机”,仍然是开发者的职责。

Q. main 函数中的 @autoreleasepool 是什么?

打开 Objective-C 项目的 main.m,会看到整个 App 都被@autoreleasepool包裹着,这就是 App 的顶层 pool。运行循环在其中运行,并在每个周期清空子 pool。

Q. 可以嵌套代码块吗?

可以。pool 会像栈一样逐层叠加。内部代码块关闭时,只会清空内部 pool,外部 pool 会保持不变。

pool 像栈一样叠加,并从内部开始清空
pool 像栈一样叠加,并从内部开始清空

简单总结,@autoreleasepool就是一个在指定时机清空“已预约释放的临时对象”的代码块。

如果你用 Instruments 确认循环中的内存像山一样飙升,可以试着用这个代码块包住循环内部。你会清楚看到图像变得平稳。

如果你对从 MRC(Manual Reference Counting,手动引用计数)转向 ARC 的历史感兴趣,也推荐阅读之前整理的《Objective-C 内存管理历史》。祝你今天编码愉快!


参考资料

延伸阅读