不能不说的 AutoreleasePool

为什么需要 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 里面的对象越来越多,内存占用成直线上升:

不能不说的 AutoreleasePool_第1张图片
内存使用情况

那怎么样才能避免这种情况出现呢?
及时释放内存。我们可以换个初始化方法,将 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"];
    }
}
不能不说的 AutoreleasePool_第2张图片
内存使用情况

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 以双向链表的形式组合而成的。

不能不说的 AutoreleasePool_第3张图片
AutoreleasePool

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 ,并注册了两个 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

你可能感兴趣的:(不能不说的 AutoreleasePool)