美文网首页
拾陆:AutoreleasePool实现原理

拾陆:AutoreleasePool实现原理

作者: FY_Chao | 来源:发表于2020-11-09 16:33 被阅读0次

    前言

    通过前面runloop文章中,我们知道在 mainRunloop 存在两个关于 autoreleasePoolRunLoopObserver,分别监听了 runloop 的 ①. 进入(Entry)、②. BeforeWaiting(准备进入休眠)和Exit(即将退出Loop)

    1. 进入(Entry): 监听到进入后会调用 _objc_autoreleasePoolPush()函数创建自动释放池。
    2. 准备进入休眠(BeforeWaiting):调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧池并创建新池。
    3. 即将退出Loop(Exit):调用_objc_autoreleasePoolPop() 来释放_autoreleasePool

    所以基本上我们不需要手动创建 AutoreleasePool。

    NSAutoreleasePool

    在 MRC 的时代,我们通过 NSAutoreleasePool 类或者@autoreleasepool创建自动释放池子,并调用对象的autorelease方法,将对象放入自动释放池子中。

    // main.m MRC环境
    #import <Foundation/Foundation.h>
    #import "Person.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *p = [[Person new] autorelease];
            NSLog(@"%@",p);
        }
        return 0;
    }
    
    

    通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m我们将代码编译生成 C++ 代码。

    image

    代码中的@autoreleasepool{}会转换成__AtAutoreleasePool的结构体对象。

    struct __AtAutoreleasePool {
      __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} // 构造函数
      ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}  // 析构函数
      void * atautoreleasepoolobj;
    };
    
    

    在上图中__AtAutoreleasePool __autoreleasepool;首先会调用结构体的构造函数objc_autoreleasePoolPush(),将对象放入自动释放池中,再当结构体的局部对象__autoreleasepool离开作用域后,调用析构函数objc_autoreleasePoolPop(atautoreleasepoolobj)对自动释放池内的对象进行一次release操作。

    AutoreleasePool 结构

    autoreleasepool通过__AtAutoreleasePool的构造函数、析构函数来创建和释放的了,那么autoreleasepool又是如何组织、存放自动释放池的对象的呢?

    让我通过 obj4 的源码来一探究竟,首先我们可以在 NSObject.mm 找到 objc_autoreleasePoolPush的实现。它主要是调用了AutoreleasePoolPage类的类方法push,返回AutoreleasePoolPage对象。

    objc_autoreleasePoolPop函数中也是对AutoreleasePoolPage对象进行操作。可以推断出autoreleasepool自定释放池对自动释放对象的管理正是通过AutoreleasePoolPage对象来实现的。

    class AutoreleasePoolPage 
    {
    
        static size_t const SIZE = 
    #if PROTECT_AUTORELEASEPOOL
            PAGE_MAX_SIZE;  // must be multiple of vm page size
    #else
            PAGE_MAX_SIZE;  // size and alignment, power of 2
    #endif
        magic_t const magic;
        id *next;
        pthread_t const thread;
        AutoreleasePoolPage * const parent;
        AutoreleasePoolPage *child;
        uint32_t const depth;
        uint32_t hiwat;
        // ... other methods
    }
    

    AutoreleasePoolPage类中定义了Page的空间大小PAGE_MAX_SIZE = 4096 个字节,除去上面代码中存放内部的成员变量外,所有剩下的空间都是用来存放 autorelease 的对象地址(8字节)。

    成员变量next指向下一个自动释放对象地址的指针存放在当前的page的位置,而且每个 page 通过 parentchild 通过双向链表的形式连接。 thread记录当前page所在的线程,AutoreleasePool是与线程一一对应的.

    image

    自动释放的对象从上图的红色 begin 箭头处开始存放,当一个 page 存放达到上限后,会在创建一个 page 并通过parentchild两个指针关联这两个page对象。以此类推每个page都存放了。

    AutoreleasePool 工作流程

    我们知道了 AutoreleasePool 两个主要的数据结构:__AtAutoreleasePoolAutoreleasePoolPage。现在来总结下 AutoreleasePool 工作的整个流程。

    将最开始的代码@autoreleasepool{}替换成__AtAutoreleasePool的实现。

    
    int main(int argc, const char * argv[]) {
    //    @autoreleasepool {
        __AtAutoreleasePool atautoreleasepoolobj = objc_autoreleasePoolPush();
            Person *p = [[Person new] autorelease];
            NSLog(@"%@",p);
        objc_autoreleasePoolPop(atautoreleasepoolobj)
    //  }
        return 0;
    }
    

    objc_autoreleasePoolPush

    objc_autoreleasePoolPush()函数:

    #define POOL_BOUNDARY = nil
        static inline void *push() 
        {
            id *dest;
            if (DebugPoolAllocation) {
                dest = autoreleaseNewPage(POOL_BOUNDARY);
            } else {
                dest = autoreleaseFast(POOL_BOUNDARY);
            }
            assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
            return dest;
        }
    
    

    首先会将POOL_BOUNDARY(一个nil对象)入栈,并返回当前压入栈中的地址值,用作 pop 时的终点。调用autorelease方法的对象会调用到 AutoreleasePoolPageautoreleaseFast 函数,添加到 page 中,在objc_autoreleasePoolPop函数调用时会将POOL_BOUNDARY的地址值传入,用作自动释放池pop的停止边界。

    当然系统会考虑到很多情况,比如当前的page情况比如是否存在、page是否满了。大概的过程如下:

    • 如果存在page、且未满会直接通过next指针将对象地址存放在next中,存在page。
    • 如果page 存在且满了,会创建一个新的page再压入POOL_BOUNDARY,再添加autorelease对象,然后通过parentchild将上下两个page关联
    • 如果page都不存在那么会创建一个新的page、压入POOL_BOUNDARY后再添加autorelease对象。

    objc_autoreleasePoolPop

    objc_autoreleasePoolPop(atautoreleasepoolobj)自动释放池的开始释放。其中atautoreleasepoolobj就是我们push时返回的边界地址(POOL_BOUNDARY)。objc_autoreleasePoolPop对调用AutoreleasePoolPagepop方法,如下。

    // token == atautoreleasepoolobj  == 每次 push 时的`POOL_BOUNDARY `
      static inline void pop(void *token) 
        {
            AutoreleasePoolPage *page;
            id *stop;
    
            if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
                // Popping the top-level placeholder pool.
                if (hotPage()) {
                    // Pool was used. Pop its contents normally.
                    // Pool pages remain allocated for re-use as usual.
                    pop(coldPage()->begin());
                } else {
                    // Pool was never used. Clear the placeholder.
                    setHotPage(nil);
                }
                return;
            }
    
            page = pageForPointer(token);
            stop = (id *)token;
            if (*stop != POOL_BOUNDARY) {
                if (stop == page->begin()  &&  !page->parent) {
                    // Start of coldest page may correctly not be POOL_BOUNDARY:
                    // 1. top-level pool is popped, leaving the cold page in place
                    // 2. an object is autoreleased with no pool
                } else {
                    // Error. For bincompat purposes this is not 
                    // fatal in executables built with old SDKs.
                    return badPop(token);
                }
            }
    
            if (PrintPoolHiwat) printHiwat();
    
            page->releaseUntil(stop);
    
            // memory: delete empty children
            if (DebugPoolAllocation  &&  page->empty()) {
                // special case: delete everything during page-per-pool debugging
                AutoreleasePoolPage *parent = page->parent;
                page->kill();
                setHotPage(parent);
            } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
                // special case: delete everything for pop(top) 
                // when debugging missing autorelease pools
                page->kill();
                setHotPage(nil);
            } 
            else if (page->child) {
                // hysteresis: keep one empty child if page is more than half full
                if (page->lessThanHalfFull()) {
                    page->child->kill();
                }
                else if (page->child->child) {
                    page->child->child->kill();
                }
            }
        }
    

    主要是三个操作

    • 使用 pageForPointer 获取当前 token 所在的 page,
    • 调用 releaseUntil 方法释放栈中的对象,直到 stop 边界(即push时返回的POOL_BOUNDARY地址)。
    • 调用 child 的 kill 方法,删除空的child page 对象。

    延伸1: AutoReleasePool的循环嵌套

    有时候我们会碰到AutoReleasePool嵌套的情况,那么自动释放的对象又是如何组织和管理的呢?

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSObject *obj1 = [[NSObject alloc] init];
            
        @autoreleasepool {
            NSObject *obj2 = [[NSObject alloc] init];
            NSObject *obj3 = [[NSObject alloc] init];
            
              @autoreleasepool {
                  NSObject *obj4 = [[NSObject alloc] init];
                }
          }
        }
        return 0;
    }
    

    按照我们之前对 autoreleasepool的理解后,我们可以将代码转换成:

    int main(int argc, const char * argv[]) {
         atautoreleasepoolobj1 = objc_autoreleasePoolPush();
            NSObject *obj1 = [[NSObject alloc] init];
             atautoreleasepoolobj2 = objc_autoreleasePoolPush();
            NSObject *obj2 = [[NSObject alloc] init];
            NSObject *obj3 = [[NSObject alloc] init];
               atautoreleasepoolobj3 = objc_autoreleasePoolPush();
                  NSObject *obj4 = [[NSObject alloc] init];
                objc_autoreleasePoolPop(atautoreleasepoolobj3)
          objc_autoreleasePoolPop(atautoreleasepoolobj2)
        objc_autoreleasePoolPop(atautoreleasepoolobj1)
        return 0;
    }
    

    每层的嵌套,都会调用一次objc_autoreleasePoolPush函数,在objc_autoreleasePoolPush时会有一次入栈操作(POOL_BOUNDARY入栈)。作为这一层autoreleasepoolpop的停止边界,大概的结构如下图。

    image

    延伸2: Runloop 与AutoReleasePool的关系

    注意:在上文提到过在mainRunloop中存在RunLoopObserver,分别监听了 runloop 的 ①. 进入(Entry)、②. BeforeWaiting(准备进入休眠)和Exit(即将退出Loop),会自动调用_objc_autoreleasePoolPush()_objc_autoreleasePoolPop() 所以一般情况下我们可以不用自己手动的去创建autoreleasePool

    延伸3: autorelease对象在什么时候释放

    autorelease对象的释放在不同场景下释放的时机不一样,

    • autorelease对象直接是由@autorelasepool{}包裹,那么autorelease对象会在@autorelasepool{}的大括号之后释放。
    • 没有显式使用@autorelasepool{},会根据某次Runloop循环中调用_objc_autoreleasePoolPop()释放(可能是在runloop休眠之前或者退出runloop时)。

    tip: ARC模式下,方法内的局部变量会在方法的大括号之后就会被释放,ARC模式下可能直接插入的 release 方法。

    相关文章

      网友评论

          本文标题:拾陆:AutoreleasePool实现原理

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