在 Swift 的 ARC 时代,虽然我们不再需要手动调用 retain 和 release,但底层依然保留了 Autoreleasepool(自动释放池) 机制。它是为了解决“对象需要在稍后某个时刻释放”的需求,尤其是在处理 Objective-C API 或大量临时对象时。
以下是它的核心工作原理:
1. 核心结构:双向链表
Autoreleasepool 在底层并不是一个简单的数组,而是由多个 AutoreleasePoolPage 组成的双向链表。
- Page 结构:每个 Page 占据 4096 字节(虚拟内存的一页)。除了存储自身元数据的头部信息外,剩下的空间用来存放对象的地址。
- 栈式管理:它的工作方式类似于栈。新加入的对象地址会被依次压入 Page 中,如果当前 Page 满了,就会创建一个新的 Page 并通过
parent和child指针连接。
2. 运作流程:Push, Add, Pop
Autoreleasepool 的生命周期通过三个核心操作管理:
- objc_autoreleasePoolPush: 每当进入一个池子(例如执行
autoreleasepool { ... }),运行时系统会在当前 Page 的位置压入一个 Sentinel(哨兵对象,值为 0) 。它标记了当前池子的起始边界。 - objc_autorelease: 当一个对象被标记为
autorelease时,它的地址会被存入 Page 中。此时对象的引用计数不会立即减少,它被池子“持有”了。 - objc_autoreleasePoolPop: 当代码块结束,系统会执行 Pop 操作,并传入对应的哨兵地址。它会从栈顶开始向下回溯,给路径上遇到的每一个对象发送一条
release消息,直到遇到哨兵为止。
3. Swift 中的使用场景
在纯 Swift 代码中,ARC 倾向于立即释放,因此 autoreleasepool 的出场率较低。但在以下两种情况它至关重要:
A. 处理大量循环中的临时对象
如果你在一个循环中创建了成千上万个 UIImage 或 Data(这些通常是 Obj-C 框架下的),它们默认会在当前的 RunLoop 结束时才释放。这会导致内存瞬间飙升。
Swift
for _ in 0..<1000000 {
autoreleasepool {
let image = UIImage(named: "large_image")
// 使用 image 处理业务
} // 每次循环结束,image 都会在这里立即执行 release
}
B. 兼容 Objective-C 框架
当调用一些返回“非持有引用”对象的 Obj-C 方法时,这些对象会被自动放入池中。
4. 线程关系
- 每个线程都有自己的 Autoreleasepool 堆栈。
- 主线程的
RunLoop会在每一轮循环开始时自动进行一次 Push,在结束前进行一次 Pop。因此,普通代码即便不写池子,对象也会在下一帧渲染前被清理。 - 手动创建的线程(如使用
Thread且没有 RunLoop)则需要手动管理,否则autorelease对象会产生泄漏。
总结
Autoreleasepool 就像一个延迟释放的容器。它通过 Page 链表结构高效地记录对象,并在池子关闭时集中“秋后算账”,统一发送释放信号。它是平衡内存回收时机与执行效率的重要工具。
英文版
8-22. [Memory Management] How does autoreleasepool work?
In the Swift ARC era, while we no longer need to manually call retain and release, the underlying Autoreleasepool mechanism remains. It exists to solve the requirement of "releasing an object at a later point in time," especially when dealing with Objective-C APIs or a large number of temporary objects.
Here is its core working principle:
1. Core Structure: Doubly Linked List
At the lower level, an Autoreleasepool is not a simple array but a doubly linked list composed of multiple AutoreleasePoolPage structures.
- Page Structure: Each page occupies 4096 bytes (one page of virtual memory). In addition to storing metadata in the header, the remaining space is used to store object addresses.
- Stack-based Management: It works similarly to a stack. New object addresses are pushed into the Page sequentially. If the current Page is full, a new Page is created and connected via
parentandchildpointers.
2. Operational Flow: Push, Add, Pop
The lifecycle of an Autoreleasepool is managed through three core operations:
- objc_autoreleasePoolPush: Whenever entering a pool (e.g., executing
autoreleasepool { ... }), the runtime system pushes a Sentinel (a sentinel object with a value of 0) at the current Page position. It marks the starting boundary of the current pool. - objc_autorelease: When an object is marked as
autorelease, its address is stored in the Page. At this point, the object's reference count does not decrease immediately; it is "held" by the pool. - objc_autoreleasePoolPop: When the code block ends, the system executes a Pop operation and passes the corresponding sentinel address. It backtracks from the top of the stack, sending a
releasemessage to every object it encounters until it reaches the sentinel.
3. Usage Scenarios in Swift
In pure Swift code, ARC tends to release objects immediately, so the usage of autoreleasepool is relatively low. However, it is crucial in the following two situations:
A. Handling Temporary Objects in Large Loops
If you create thousands of UIImage or Data objects (which are often from Objective-C frameworks) inside a loop, they are released by default only when the current RunLoop ends. This can cause a sudden memory spike.
Swift
for _ in 0..<1000000 {
autoreleasepool {
let image = UIImage(named: "large_image")
// Process the image
} // At the end of each iteration, 'image' is immediately released here
}
B. Compatibility with Objective-C Frameworks
When calling Objective-C methods that return "non-owning reference" objects, those objects are automatically placed into the pool.
4. Relationship with Threads
- Each thread has its own Autoreleasepool stack.
- The
RunLoopof the main thread automatically performs a Push at the start of every iteration and a Pop before it ends. Therefore, even without an explicit pool, objects in normal code are cleaned up before the next frame renders. - Manually created threads (e.g., using
Threadwithout a RunLoop) require manual management; otherwise,autoreleaseobjects will cause memory leaks.
Summary
An Autoreleasepool acts like a deferred-release container. It efficiently records objects using a linked list of Pages and performs a "final reckoning" by sending release signals collectively when the pool closes. It is an essential tool for balancing memory reclamation timing with execution efficiency.