为什么需要 AutoreleasePool
1. 延长对象生命周期
我们都知道,系统内存是有限的,要想系统一直正常高效运行着,就需要我们合理地管理内存,不需要的内存就应该及时释放。在 Objective-C 早期年代采用的是 MRC 来管理内存,需要我们自己在合适的位置申请和释放内存。在 LLVM 3.0 开始,Objective-C 引入 ARC(自动引用计数),就无需要我们手动管理内存了,系统会自动管理内存:
- (void)run {
id __strong obj = [[NSObject alloc] init]; //生成并持有对象,retainCount = 1
//... 使用 obj
} //obj 超出作用域,强引用失效,retainCount = 0
正常情况下,使用 release 都能帮助我们及时回收内存,防止内存泄漏。我们接着看下一种情况:
- (NSObject *)getObj {
id __strong obj = [[NSObject alloc] init]; //生成并持有对象,retainCount = 1
return obj;
}
- (void)run {
id __strong obj = [self getObj]; //持有对象, retainCount = 2
/*
使用 obj
*/
}//obj 超出作用域,强引用失效,retainCount = 1
我们发现到最后 obj 的 retainCount = 1 并没有被销毁,那么 release 能解决这个问题吗?
- (NSObject *)getObj {
NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
[obj release]; // 此处加入 release,导致 obj 提前释放
return obj;
[obj release]; // 这儿根本没什么作用...
}
所以,我们需要扩大 obj 的生命周期,而 AutoreleasePool 正好可以解决这个问题:被加入 AutoreleasePool 的对象,不会立即释放,而是在 AutoreleasePool 结束时调用 [obj release]
来保证对象在超出指定生存范围时能够自动并正确释放。所以上述方法正确的实现应该是:
- (NSObject *)getObj {
NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
[obj autorelease]; //系统自动插入,先将 obj 放入最近的 AutoreleasePool,稍后释放(retainCount - 1)
return obj;
}
2. 降低内存峰值
先看一个常见的面试题:
for (int i = 0; i < 100000000; i++) {
NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
str = [str stringByAppendingString:@" - world"];
}
上述代码能正常运行吗?如不能,请解释原因并给出优化意见。
熟悉内存管理的同学应该知道,stringWithFormat:
返回的是一个 Autorelease 对象,所以每次循环生成的 str 都不会立即释放,而是放入最近的 AutoreleasePool,当前 AutoreleasePool 需要等待当前线程 RunLoop 来释放,当前线程 RunLoop 又一直在处理 for 循环等事件一直处于活跃状态并不会释放 AutoreleasePool,这样就会导致 AutoreleasePool 里面的对象越来越多,内存占用成直线上升:
那怎么样才能避免这种情况出现呢?
及时释放内存。我们可以换个初始化方法,将 stringWithFormat:
换成了 alloc + initWithFormat:
,这样对象就能立即释放。另一个比较好的方案是加入局部 AutoreleasePool ,这样也能避免在复杂情况下,去一个一个方法去确认是否返回 Autorelease 对象,手动加入的 AutoreleasePool,在作用域过后就立即清空池内所有对象。
for (int i = 0; i < 100000000; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
str = [str stringByAppendingString:@" - world"];
}
}
alloc
new
copy
mutableCopy
等方法会生成并持有对象,而其他类似[NSMutableArray array]
的方法会生成对象但不持有,返回的是 Autorelease 对象。
AutoreleasePool 原理
前面我们知道了 AutoreleasePool 的实践应用,那它究竟是怎样工作的呢,首先我们出它的结构说起,通过使用 clang -rewrite-objc
命令将下面的 Objective-C 代码重写成 C++ 代码我们可以得知:
@autoreleasepool {
//...
}
实际上相当于
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
//...
objc_autoreleasePoolPop(atautoreleasepoolobj);
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
所以 AutoreleasePool 实际上是以 AutoreleasePoolPage 的形式在工作。我们来看看 AutoreleasePoolPage 在 NSObject.mm 中的定义:
class AutoreleasePoolPage {
magic_t const magic; // 完整性校验
id *next; // 指向下一个内存为空的地址
pthread_t const thread; // 当前线程
AutoreleasePoolPage * const parent; // 构造双向链表的指针
AutoreleasePoolPage *child; // 构造双向链表的指针
uint32_t const depth;
uint32_t hiwat;
};
AutoreleasePool 并没有单独的结构,是由若干个 AutoreleasePoolPage 以双向链表的形式组合而成的。
AutoreleasePoolPage 每个实例对象都会开辟 4096 bytes 内存(一页虚拟内存的大小),除开底部用于存储 AutoreleasePoolPage 的成员变量的空间,其余都用来储存加入到自动释放池的对象。
加入 AutoreleasePool
当你对一个对象 obj 发送 autorelease
消息时,如果当前线程不存在 AutoreleasePool,则会先生成 AutoreleasePool 对象:void *atautoreleasepoolobj = objc_autoreleasePoolPush();
,每当执行一次 objc_autoreleasePoolPush
, runtime 就向当前的 AutoreleasePoolPage 中 add 进一个哨兵对象(POOL_SENTINEL), atautoreleasepoolobj 即为返回的哨兵对象(POOL_SENTINEL)。接着 obj 就会被放入 next 指针所指的地址,然后 next 指向下一个内存为空的地址,如果当前 AutoreleasePoolPage 被占满,就会生成新的 AutoreleasePoolPage 来存放 obj,并连接链表。
释放 AutoreleasePool
从最新加入的对象一直向前清理,给每个对象发送 release
消息,直到哨兵对象(POOL_SENTINEL)位置,并回移 next
指针到正确的位置。
AutoreleasePool 对象何时释放
我们从给对象 obj 发送 autorelease
消息开始说起:
-(id) autorelease
{
return _objc_rootAutorelease(self);
}
_objc_rootAutorelease(id obj)
{
assert(obj);
return obj->rootAutorelease();
}
可以看到这个方法里只是简单的调了一下 _objc_rootAutorelease()
,继续跟进:
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
可以看到:[obj autorelease]
实际上是调用了 autoreleaseFast(obj)
,在 autoreleaseFast()
里面会判断当前是否存在 AutoreleasePoolPage ,如果不存在则调用 autoreleaseNoPage(obj)
,我们接着看这个方法:
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
assert(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
pthread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
return page->add(obj);
}
可以发现,经过一系列条件筛选,当不存在 AutoreleasePoolPage 时会生成新的 AutoreleasePoolPage 对象: AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
,这就表明:
当我们对一个对象发送 autorelease 消息时,都会被加入到最近的 AutoreleasePool ,不存在则先创建
- 对于手动添加的
@autoreleasepool { }
,里面的对象会在}
之后接收到release
消息 - 对于系统自动创建的
autoreleasepool
, 里面的对象与当前线程的 RunLoop 有关- 当前线程的 RunLoop 处于未开启状态时,
autoreleasepool
会在线程销毁时一并清空 - 当前线程的 RunLoop 处于开启状态时(主线程的 RunLoop 会自动开启,其他需要手动),RunLoop 会在合适的时机管理(push,pop)
autoreleasepool
- 当前线程的 RunLoop 处于未开启状态时,
对于主线程,系统会帮我们自动开启 RunLoop ,并注册了两个 Observer,其回调都是
_wrapRunLoopWithAutoreleasePoolHandler()
,第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用_objc_autoreleasePoolPush()
创建自动释放池。其 order 是 -2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit(即将退出Loop)时调用_objc_autoreleasePoolPop()
来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。在主线程执行的代码,通常是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
Swift 中的 AutoreleasePool
实践上,Swift 中的 AutoreleasePool 是桥接于 Objective-C,我们在 Swift 中使用 autoreleasepool { }
其实就是 Objective-C 中的那个。在过去这段时间,Swift 对 ARC 做过很多优化,好像并没有了 AutoreleasePool 存在的必要性。然而真的不需要了吗?我们看下述代码:
guard let file = Bundle.main.path(forResource: "bigImage", ofType: "png") else {
return
}
for i in 0..<1000000 {
let url = URL(fileURLWithPath: file)
let imageData = try! Data(contentsOf: url)
}
还会引起内存的问题吗?答案是:会。因为 Data(contentsOf: url)
实际上是桥接于 [NSData dataWithContentsOfURL]
,不幸的,还是会返回 autorelease 对象,同样适用 autoreleasepool { }
能解决这个问题:
autoreleasepool {
let url = URL(fileURLWithPath: file)
let imageData = try! Data(contentsOf: url)
}
所以,AutoreleasePool 在 Swift 开发中仍然有用,因为在 UIKit 和 Foundation 中仍然存在遗留的 Objective-C 类 autorelease,但是由于 ARC 的优化,你在处理 Swift 类时可能不需要担心它。
总结
- Autorelease 是通过推迟对对象发送
release
消息来延长对象生命周期的 - 每一个接收到
autorelease
消息的对象都会被加入最近的 AutoreleasePool(如果没有就创建),然后会在当前线程销毁时或者当前 Runloop 切换状态(准备进入休眠和即将退出)时释放,所以无需担心autorelease
对象的内存问题 - AutoreleasePool 并没有单独的结构,是由若干个 AutoreleasePoolPage 以双向链表的形式组合而成的
- AutoreleasePool 是通过哨兵对象(POOL_SENTINEL)来完成清空的,嵌套的 AutoreleasePool 相当于多个哨兵对象(POOL_SENTINEL)
参考
深入理解RunLoop
自动释放池的前世今生
黑幕背后的Autorelease
带着问题看源码----子线程AutoRelease对象何时释放
各个线程 Autorelease 对象的内存管理
does NSThread create autoreleasepool automatically now?
@autoreleasepool uses in 2019 Swift