美文网首页
不能不说的 AutoreleasePool

不能不说的 AutoreleasePool

作者: klike | 来源:发表于2019-05-31 18:12 被阅读0次

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

    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

          本文链接:https://www.haomeiwen.com/subject/ctvgtctx.html