iOS自动释放池的实现原理分析2

2,090 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情

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函数中标记的开始结束之间的部分。

多个对象是连续存放的,__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个对象;第二页中只有一个对象。