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

引用计数:
-当我们创建一个实例对象,它的引用计数为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(token, page, stop);
}

templatestatic void  popPage(void *token, AutoreleasePoolPage *page, id *stop){
    page->releaseUntil(stop);//向对象发送release消息,传入的stop是哨兵对象的地址
    ...
    if (page->child) {
       if (page->lessThanHalfFull()) {
            page->child->kill();//如果当前存放的对象少于一半,删除子节点中的内容。【保留了parent-this节点】
       }
       else if (page->child->child) {
            page->child->child->kill();//存放对象多余一半,且子节点的子节点存在,删除子节点的子节点。【保留下了parent-this-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开发:自动释放池的实现原理分析)