美文网首页iOS面试题iOS
浅谈AutoreleasePool底层实现原理

浅谈AutoreleasePool底层实现原理

作者: 二猪哥 | 来源:发表于2019-02-18 15:06 被阅读0次

    AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行。

    AutoreleasePool创建和释放

    • App启动后,苹果在主线程 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 了。

    也就是说AutoreleasePool创建是在一个RunLoop事件开始之前(push),AutoreleasePool释放是在一个RunLoop事件即将结束之前(pop)。
    AutoreleasePool里的Autorelease对象的加入是在RunLoop事件中,AutoreleasePool里的Autorelease对象的释放是在AutoreleasePool释放时。

    AutoreleasePool实现原理

    在终端中使用clang -rewrite-objc命令将下面的OC代码重写成C++的实现(参考:使用clang将OC代码转为C++
    ):
    1)@autoreleasepool实质上是一个__AtAutoreleasePool的结构体对象;
    下面是main 函数的C++实现,声明了一个__AtAutoreleasePool对象,然后调用应用程序入口函数;

    int main(int argc, char * argv[]) {
        /* @autoreleasepool */ {
            __AtAutoreleasePool __autoreleasepool;
            return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
        }
    }
    

    __AtAutoreleasePool的结构如下所示:

    extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
    extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
    
    struct __AtAutoreleasePool {
      __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
      ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
      void * atautoreleasepoolobj;
    };
    

    可以看出来,当申明了一个对象__autoreleasepool,相当于调用了objc_autoreleasePoolPush()函数,该函数的作用即向堆栈内压入一个“自动释放池”;而当int main()函数执行完毕后,执行__autoreleasepool的析构函数objc_autoreleasePoolPop(atautoreleasepoolobj),用于释放“自动释放池”;所以main()函数里可以大致这样理解:

    int main(int argc, char * argv[]) {
        /* @autoreleasepool */ {
            //创建自动释放池
            __AtAutoreleasePool __autoreleasepool = objc_autoreleasePoolPush();
            //TODO 执行各种操作,将对象加入自动释放池
            
            //释放自动释放池
            objc_autoreleasePoolPop(__autoreleasepool)
        }
    }
    

    2) AutoreleasePool的本质

    已经有人在苹果的官方源码里找到了关于AutoreleasePool的底层实现NSObject.mm里,源码地址:https://opensource.apple.com/source/objc4/objc4-532/runtime/NSObject.mm.auto.html
    objc_autoreleasePoolPush()objc_autoreleasePoolPop(__autoreleasepool)的实现如下:

    void* objc_autoreleasePoolPush(void)
    {
        if (UseGC) return NULL; //如果使用垃圾回收机制
        return AutoreleasePoolPage::push();
    }
    
    void objc_autoreleasePoolPop(void *ctxt)
    {
        if (UseGC) return;
    
        // fixme rdar://9167170
        if (!ctxt) return;
    
        AutoreleasePoolPage::pop(ctxt);
    }
    

    从上面可以发现,C++类AutoreleasePoolPage才是实际的实现所在,找到AutoreleasePoolPage:

    class AutoreleasePoolPage 
    {
    #define POOL_SENTINEL 0
        static size_t const SIZE = 
    #if PROTECT_AUTORELEASEPOOL
            4096;  // must be multiple of vm page size
    #else
            4096;  // 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;   //page的序号?
        uint32_t hiwat;
        ...
    }
    

    去除了一些不重要的代码,可以看出这是一个典型的双向列表结构,每个Page大小为4096 Byte,所以AutoreleasePool实质上是一个双向AutoreleasePoolPage列表;接下来分析一下自动释放池的工作过程:

    创建自动释放池

    void* objc_autoreleasePoolPush()内部实际调用的是AutoreleasePoolPage::push()函数,其实现如下:

    static inline void *push() 
        {
            if (!hotPage()) {
                setHotPage(new AutoreleasePoolPage(NULL));
            } 
            id *dest = autoreleaseFast(POOL_SENTINEL);
            assert(*dest == POOL_SENTINEL);
            return dest;
        }
    

    hotPage()是找出当前的正在使用的page,第一次调用时hotPage为NULL,所以新建一个parent=NULL的AutoreleasePoolPage对象作为自动释放池加入栈中,并将其设置为hotPage,然后返回POOL_SENTINEL的地址赋值给main()函数里的变量 __autoreleasepool;

    然后将一个哨兵对象POOL_SENTINEL压入栈顶,即调用autoreleaseFast(POOL_SENTINEL)

    static inline id *autoreleaseFast(id obj)
        {
            AutoreleasePoolPage *page = hotPage();
            if (page && !page->full()) {
                return page->add(obj);
            } else {
                return autoreleaseSlow(obj);
            }
        }
    

    添加对象进自动释放池

    可以看出,如果当前有page并且没有满,则直接将对象入栈顶(page->add(obj)):

    id *add(id obj)
        {
            assert(!full());
            unprotect();
            *next++ = obj;
            protect();
            return next-1;
        }
    

    将对象压入栈顶,然后将栈顶指针下移;
    如果上述autoreleaseFast(id obj)中的page已经满了,则执行autoreleaseSlow(obj)

    id *autoreleaseSlow(id obj)
        {
            AutoreleasePoolPage *page;
            page = hotPage();
            //如果没有page,则新建一个自动释放池,并添加obj对象进释放池
            if (!page) {
                objc_autoreleaseNoPool(obj);
                return NULL;
            }
            //如果当前hotPage已经满了,则以链表的形式新增一个page并添加到当前page的后面,然后将此设置为hotPage;
            do {
                if (page->child) page = page->child;
                else page = new AutoreleasePoolPage(page);
            } while (page->full());
    
            setHotPage(page);
            return page->add(obj);
        }
    

    基本逻辑即,如果page不存在则,创建新的自动释放池(PoolPage),并将对象加进池子;如果已经存在自动释放池在栈中,且hotPage满了,则遍历其子page,如果存在没满(page->full()==NO)的子page,则将该子page设置为hotPage,否则如果都满了,则以最后一个子page为父page,新建一个page,插入当前的page链表,同样设置该新建的page为hotPage,然后将自动释放对象加入page;

    销毁自动释放池

    销毁自动释放池的调用方式是:

    void AutoreleasePoolPage::pop(void *token)
    

    token 即push()的返回值,实际上就是POOL_SENTINEL的地址(__autoreleasepool)通过该地址即可找到所在Page的地址指针

    static inline void pop(void *token) 
        {
            AutoreleasePoolPage *page;
            id *stop;
    
            if (token) {
                page = pageForPointer(token); //找到所在的page地址
                stop = (id *)token; //POOL_SENTINEL的地址,从栈顶释放对象直到这个位置
            } else {
                // Token 0 is top-level pool
                page = coldPage();
                stop = page->begin();
            }
    
            page->releaseUntil(stop); //对自动释放池中对象调用objc_release()进行释放
    
            // memory: delete empty children
            // hysteresis: keep one empty child if this page is more than half full
            // special case: delete everything for pop(0)
            if (!token) {
                page->kill();
                setHotPage(NULL);
            } else if (page->child) {
                if (page->lessThanHalfFull()) {
                    page->child->kill();
                }
                else if (page->child->child) {
                    page->child->child->kill();
                }
            }
        }
    

    该过程主要分为两步:
    一、page->releaseUntil(stop),对栈顶(page->next)到stop地址(POOL_SENTINEL)之间的所有对象调用objc_release(),进行引用计数减1;
    二、清空page对象page->kill(),有两句注释:

    // hysteresis: keep one empty child if this page is more than half full
    // special case: delete everything for pop(0)
    

    除非是pop(0)方式调用,这样会清理掉所有page对象;否则,在当前page存放的对象大于一半时,会保留一个空的子page,这样估计是为了可能马上需要新建page节省创建page的开销吧.

    对应代码

    if (page->child) {
               if (page->lessThanHalfFull()) {
                   page->child->kill(); //全部删除
               }
               else if (page->child->child) {
                   page->child->child->kill();//保留一个子page
               }
           }
    

    如果当前的page中存放的对象少于一半,则子page全部删除;如果当前当前的page存放的多余一半(意味着马上将要满),则保留一个子page,节省创建新page的开销;

    相关文章

      网友评论

        本文标题:浅谈AutoreleasePool底层实现原理

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