美文网首页iOS开发进阶
iOS开发进阶:自动释放池的实现原理分析

iOS开发进阶:自动释放池的实现原理分析

作者: __Null | 来源:发表于2021-12-20 23:19 被阅读0次

    引用计数:
    -当我们创建一个实例对象,它的引用计数为1;
    -当我们向一个对象发送retain消息,它的引用计数+1;
    -当我们向一个对象发送release消息,它的引用计数-1;
    -当我们向一个对象发送autorelease消息,它的引用计数会在当前自动释放池的末尾-1;
    -当一个对象的引用计数减到0,它的内存会被回收。

    在MRC下如下代码:

    @autoreleasepool{
        NSObject *obj = [[NSObject alloc] init] autorelease];
    }
    

    通过clang命令可以转换成cpp源代码,在代码中包含__AtAutoreleasePool结构体如下:

    //自动释放池结构体
    struct __AtAutoreleasePool {
        __AtAutoreleasePool(){/*构造函数*/
            atautoreleasepoolobj = objc_autoreleasePoolPush();
        }
        ~__AtAutoreleasePool(){/*析构函数*/
            objc_autoreleasePoolPop(atautoreleasepoolobj);
        }
        void *atautoreleasepoolobj;
    };
    

    该结构体有一个void *类型成员变量atautoreleasepoolobj,保存了构造函数中调用objc_autoreleasePoolPush()的返回值,在析构函数中调用objc_autoreleasePoolPop(atautoreleasepoolobj),将该值传递了进去。

    搜索源码,可以看到objc_autoreleasePoolPush()内部调用了AutoreleasePoolPage::push(),objc_autoreleasePoolPop(ctxt)内部调用了AutoreleasePoolPage::pop(ctxt)

    重点来到了AutoreleasePoolPageAutoreleasePoolPage继承自AutoreleasePoolPageData,它的结构如下:

    struct AutoreleasePoolPageData{
        magic_t const magic; // 16
        __unsafe_unretained id *next; // 8
        pthread_t const thread; // 8
        AutoreleasePoolPage * const parent; // 8
        AutoreleasePoolPage *child; // 8
        uint32_t const depth; // 4
        uint32_t hiwat;  // 4
    }
    
    class AutoreleasePoolPage : private AutoreleasePoolPageData{}
    
    • magic:用来校验AutoreleasePoolPage的结构是否完整
    • next:指向栈顶,也就是最新入栈的autorelease对象的下一个位置
    • thread:指向当前线程
    • parent:指向父节点
    • child:指向子节点
    • depth:表示链表的深度,也就是链表节点的个数
    • hiwat:表示high water mark(最高水位标记)
      通过数据结构,我们可以看到AutoreleasePoolPage是一个双向链表结构, parent指向了父节点,child指向了子节点。那么整个的自动释放池到底是怎么运作起来的呢?接下来我们将重点分析一下AutoreleasePoolPage::push()AutoreleasePoolPage::pop(ctxt)这2个方法。
    static inline void *push() {
        id *dest = autoreleaseFast(POOL_BOUNDARY);
        return dest;
    }
    

    去掉DebugPoolAllocation调试的if分支,内部调用了autoreleaseFast(POOL_BOUNDARY),返回了dest# define POOL_BOUNDARY nil,则表示push的时候传入了的POOL_BOUNDARY是一个nil,POOL_BOUNDARY也叫哨兵对象。

    static inline id *autoreleaseFast(id obj){
        AutoreleasePoolPage *page = hotPage();//获取hotPage
        if (page && !page->full()) {
           //page存在且没有满,则添加obj(这里obj是POOL_BOUNDARY)
           return page->add(obj);
        } 
        else if (page) {
           //page存在且已满,传入obj和page(这里obj是POOL_BOUNDARY)
           return autoreleaseFullPage(obj, page);
        } 
        else {
            //page不存在的情况下
           return autoreleaseNoPage(obj);
        }
    }
    

    这个函数做的是一个初步判定的工作:如果当前hotPage存在且未满,则page-add(obj);如果page存在且已满则调用autoreleaseFullPage(obj, page)page不存在的情况下调用autoreleaseNoPage(obj)。按照逻辑来时首次应当是不存在hotPage的,所以我们先看看autoreleaseNoPage(obj)

    static __attribute__((noinline)) id *autoreleaseNoPage(id obj){
        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            //是否有占位
            pushExtraBoundary = true;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            //设置一个占位
            return setEmptyPoolPlaceholder();
        }
    
        //有占位后就初始化一个page。并设置为hotPage
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        //将哨兵对象加入作为边界的标记
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        //将请求的对象加入
        return page->add(obj);
    }
    

    打印obj对象如下:

    (lldb) po obj
    <__NSBundleTables: 0x1007217a0>
    

    page有一个printAll()方法,可以打印池子中的对象。我们在执行完page->add(POOL_BOUNDARY)page->add(obj)后分别打印数据如下:

    (lldb) p this->printAll()
    AUTORELEASE POOLS for thread 0x1000ebe00
    1 releases pending.
    [0x104811000]  ................  PAGE  (hot) (cold)
    [0x104811038]  ################  POOL 0x104811038
    
    (lldb) p this->printAll()
    AUTORELEASE POOLS for thread 0x1000ebe00
    2 releases pending.
    [0x104811000]  ................  PAGE  (hot) (cold)
    [0x104811038]  ################  POOL 0x104811038
    [0x104811040]       0x1007217a0  __NSBundleTables
    

    两次打印对比发现,第二次打印比第一次多了个__NSBundleTables的实例对象。其中POOL 0x104811038就是哨兵对象,看日志这里的哨兵对象也计入了releases pending对象个数的统计中。在add(obj)函数中ret的是哨兵对象的地址。
    来到了最关键的add方法,简化该函数如下:

    id *add(id obj){
        unprotect();
        id *ret;
    
        if(....){
          ....
           ret = (id *)topEntry;
           goto done;
        }
    
        ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        ...
     done:
        protect();
        return ret;
    }
    

    在简化后的函数中,返回的地址是ret,(ret = next,即是上一次操作之后的next指向的地址),*next = objnext++。我们在main中添加如下代码:

    printf(">>>>>>\n");
    @autoreleasepool {
        NXPerson *person = [[[NXPerson alloc] init] autorelease];
        NSObject *obj = [[[NSObject alloc] init] autorelease];
    }
    printf("<<<<<<<\n");
    

    在AutoreleasePoolPage的add()函数末尾,return之前添加如下代码:

    this->printAll();
    printf("ret=%p, %p, next=%p\n\n", ret, *ret, next);
    

    然后运行查看打印的日志,日志很长,这里只留存main函数中标记的开始结束之间的部分。


    自动释放池.png

    多个对象是连续存放的,__AtAutoreleasePool构造函数返回的是插入的哨兵对象的地址,保存在atautoreleasepoolobj上,如果一个页已经满了,则会新开辟一个AutoreleasePoolPage结构,继续插入对象。

    __AtAutoreleasePool的析构函数函最终会调用AutoreleasePoolPage::pop(ctxt),这里的ctx即是构造函数返回的哨兵对象的地址, pop函数简化后的代码如下,

    static inline void pop(void *token){
        ...
        AutoreleasePoolPage *page = pageForPointer(token);//拿到当前哨兵对象岁在的页
        id *stop = (id *)token;
        return popPage<false>(token, page, stop);
    }
    
    template<bool allowDebug>static void  popPage(void *token, AutoreleasePoolPage *page, id *stop){
        page->releaseUntil(stop);//向对象发送release消息,传入的stop是哨兵对象的地址
        ...
        if (page->child) {
           if (page->lessThanHalfFull()) {
                page->child->kill();//如果当前存放的对象少于一半,删除子节点中的内容。【保留了parent?-this-child节点】
           }
           else if (page->child->child) {
                page->child->child->kill();//存放对象多余一半,且子节点的子节点存在,删除子节点的子节点。【保留下了parent-this-child-child节点】
           }
        }
    }
    

    pop()函数:内部直接调用popPage函数。popPage函数中,releaseUntil出栈操作当前对象。因为哨兵对象在该也存在,原则上该哨兵对象后面的所有对象都应该清理掉。这里是根据清理后的页的数据量大小选择性的保留1到2个空页。

    void releaseUntil(id *stop) {
        while (this->next != stop) {
            AutoreleasePoolPage *page = hotPage();
    
            //只要如果当前页空了,还没找到哨兵对象,则去父级去找(设置父级为热页)
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }
    
            page->unprotect();
            //实际打印测试中,这行代码会清楚掉页上--page->next上的对象
            AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;
    
            id obj = (id)entry->ptr;
            int count = (int)entry->count;  
    
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();
    
            if (obj != POOL_BOUNDARY) {
                for (int i = 0; i < count + 1; i++) {
                    objc_release(obj);//向非哨兵对象发送release消息。为啥要这么多次??
                }
            }
        }
        setHotPage(this);
    }
    

    releaseUntil()函数:从next指针倒着清理,向被清理的对象发送release消息,直到找到传入的哨兵对象的指针地址才会退出。如果在当前页没有找到会去父页中继续寻找。

    void kill(){
         AutoreleasePoolPage *page = this;
         while (page->child) page = page->child;
         AutoreleasePoolPage *deathptr;
         do {
            deathptr = page;
            page = page->parent;
            if (page) {
               page->unprotect();
               page->child = nil;
               page->protect();
            }
            delete deathptr;
          } while (deathptr != this);
     }
    

    kill函数会通过while循环找到最子级的页面page。然后从最子级的页面开始(deathptr = page),保存父级页面到page = page->parent,首先清理掉父节点下保存的child指针,然后删除当前子节点。注意:这里根据do-while循环的规则。如果当前page没有child,则会将当前的自己delete掉;如果有child,则会将child及其子节点delete掉,并不会delete掉自身(deathptr != this)。

    上面我们推测说一页最多放505个对象(含哨兵对象):如果我们在一个@autoreleasepool中for循环出505个对象,那么应该会有两个页:第一个页中有一个哨兵对象+504个对象;第二页中只有一个对象。

    相关文章

      网友评论

        本文标题:iOS开发进阶:自动释放池的实现原理分析

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