Autoreleasepool底层原理

Anyhong 2018年05月07日 1,208次浏览

Autoreleasepool

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    //
    }
    return 0;
}

使用 clang rewrite 命令重写 main.m 文件:

clang -rewrite-objc main.m

最终生成了一个 main.cpp 文件,进文件找到 main() 函数:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ 
    { 
        __AtAutoreleasePool __autoreleasepool; 
    }
    return 0;
}

根据注释 /* @autoreleasepool */ 可以得知 @autoreleasepool {} 被转换了一个 __AtAutoreleasePool 类型的变量 __autoreleasepool,在 main.cpp 文件里搜索一下 __AtAutoreleasePool,可以得知 __AtAutoreleasePool 是一个结构体:

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

这个结构体在构造方法 __AtAutoreleasePool() 中会调用 objc_autoreleasePoolPush() 方法,在析构方法 ~__AtAutoreleasePool() 中会调用 objc_autoreleasePoolPop() 方法。根据这两个方法的名称以及调用他们的方法类型,可以猜测这是在进行一个入栈出栈的操作,在结构体初始化的时候入栈,在结构体释放的时候出栈,也就是说 main() 函数中包含的编译器帮我们实现的方法 @autoreleasepool {} 实际的工作逻辑大概是这样的:

int main(int argc, const char *argv[]) {

    /* @autoreleasepool */
    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        // do something
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

其实在 objc runtime 源码里面就能找到 autoreleasepool 的直接使用例子:

void call_load_methods(void)
{
    ...
    void *pool = objc_autoreleasePoolPush();
    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }
        // 2. Call category +loads ONCE
        more_categories = call_category_loads();
        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);
    objc_autoreleasePoolPop(pool);
    ...
}

在进行执行类和分类load()方法的时候,手动添加了一个自动释放池,那么 objc_autoreleasePoolPush() 和 objc_autoreleasePoolPop() 内部具体是怎样实现的呢,直接结合 objc 的源码来分析,查看这两个方法的实现源码,发现这两个方法其实就是直接调用了 AutoreleasePoolPage 类的 push() 和 pop() 方法,其实到这里已经知道 AutoreleasePool 是由 AutoreleasePoolPage 类来实现的。

void *objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

AutoreleasePoolPage

AutoreleasePoolPage 的实现在 objc 的 NSObject.mm 文件中,查看源码可以知道 AutoreleasePoolPage 是一个C++类,整个类的实现代码也就几百行,源码中有一段注释对 AutoreleasePool 的实现原理进行了简单说明:

Autorelease pool implementation
A thread's autorelease pool is a stack of pointers. Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary. A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released. The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary. Thread-local storage points to the hot page, where newly autoreleased objects are stored.

大致意思就是:

一个线程的自动释放池是一系列指针的堆栈。堆栈中的指针要么是要释放的对象,要么是一个自动释放池的边界对象(POOL_BOUNDARY),一个释放池标识(pool token)就是一个指向自动释放池边界(POOL_BOUNDARY)的指针。当释放池从堆栈中pop时,所有在这个释放池哨兵指针(sentinel)后的对象都会被释放掉,栈被分成一个双向链接的页面链表(doubly-linked list of pages),页面(page)根据需要进行增加和删除,线程本地存储(TLS)指向存储了最新release对象的热页面(hot page)。

AutoreleasePoolPageLink

基础属性

简单来说,AutoreleasePool 就是由多个 AutoreleasePoolPage 组成的双向链表。还是先看 AutoreleasePoolPage 的源码,下面的代码简化了许多细节:

class AutoreleasePoolPage 
{
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;// 0xA3A3A3A3 after releasing
    static size_t const SIZE = PAGE_MAX_SIZE;
    static size_t const COUNT = SIZE / sizeof(id);

    /* 下列字段共占用 56 字节 */
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
    ...
}

#define PAGE_MAX_SIZE   PAGE_SIZE
#define	PAGE_SIZE       I386_PGBYTES
#define I386_PGBYTES    4096 /* bytes per 80386 page */

/* 
当刚好只有一个pool入栈并且它从未包含任何对象时,
EMPTY_POOL_PLACEHOLDER存储在TLS中。
当顶级(即libdispatch)入栈和出栈pool但从没使用它时,
这可以节省内存。
*/
#define EMPTY_POOL_PLACEHOLDER ((id*)1)
#define POOL_BOUNDARY nil

每个 AutoreleasePoolPage 大小是 4096 字节,为什么是4096?因为虚拟内存扇区大小就是 4096,也就是 4k 对齐,可以提升读写效率。AutoreleasePoolPage 结构中除去固定字段占用的 56 字节,剩余的内存空间可以用来存放 autorelease 对象的地址。

字段说明:

  • magic:这个字段是用来验证 AutoreleasePoolPage 结构完整性的,对 AutoreleasePool 的实现过程并无关系,这里暂时略过。
  • thread:当前 AutoreleasePoolPage 所在的线程,AutoreleasePool 是和线程一一对应的。
  • next:指向栈顶,也就是下个 autorelease 对象入栈的目标位置。
  • parent:指向父节点(上一个节点)
  • child:指向子节点(下一个节点)
  • depth:双向链表的深度(链表的节点个数)
  • hiwat:high water mark,最高水位标记,也就是记录双向链表深度达到过的最大值

宏定义:

  • POOL_BOUNDARY:这个宏表示释放池的边界
  • EMPTY_POOL_PLACEHOLDER:空 pool 的占位符

AutoreleasePoolPage

Tips:
以 alloc、copy、new、mutableCopy 开头的方法名创建的对象是自己生成并持有的。其它方法生成的对象会自动加入自动释放池中。

POOL_BOUNDARY

POOL_BOUNDARY 是 autoreleasepool 的边界标识符, autoreleasepool 是可以嵌套的,如下所示:

@autoreleasepool {
    // do something
    @autoreleasepool {
        // do something
    }
}

每开始一个新的 autoreleasepool,都会调用 push() 函数入栈一个 POOL_BOUNDARY 标识符到栈上,表示接下来开启了一个新的 autoreleasepool。

hotPage

其实就是通过 TLS 存储一下当前使用的 AutoreleasePoolPage 的地址,方面下次快速获取。为什么要使用 TLS,因为

objc_autoreleasePoolPush

步骤

前面我们已经知道了 objc_autoreleasePoolPush() 函数就是直接调用了 AutoreleasePoolPage 类的 push() 方法:

void *objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

接下来再看看 push() 函数的实现,简化后的代码如下:

static inline void *push() 
{
    id *dest = autoreleaseFast(POOL_BOUNDARY);
    return dest;
}

内部也是直接调用了 autoreleaseFast() 函数,删掉一部分对逻辑无关的代码,autoreleaseFast() 的实现大致如下:

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

这个函数共处理了三种情况:

  1. 有 hotPage 并且 page 中还有可用空间:
    这个时候直接调用 add() 方法将 autorelease 对象添加到 AutoreleasePoolPage 的可用栈里面;

  2. 有 hotPage 并且 page 中已经没有可用空间:
    当 hotPage 没有可用的栈空间了,这个时候就用 autoreleaseFullPage() 创建一个新 page;
    新创建的 page 设置为 hotPage,然后将 autorelease 对象添加到 page 的栈中;

  3. 没有 hotPage
    如果还没有 hotPage 这个时候通过 autoreleaseNoPage() 方法创建一个 page。

接下来再说说上面三种情况中用到的 add()autoreleaseFullPage()autoreleaseNoPage() 这几个函数。

add

add() 方法其实就是一个入栈的操作,将对象加入到 AutoreleasePoolPage 的栈中,然后将栈顶指针 next 移动到下一个空闲位置,简化后的代码如下:

id *add(id obj)
{
    ...
    id *ret = next; 
    *next++ = obj;
    ...
    return ret;
}

autoreleaseNoPage

autoreleaseNoPage() 其实可以作为一个初始化的方法,当内存中还没有 AutoreleasePoolPage 存在的时候,通过这个方法创建一个新的 page,也可以理解为 page 双向链表的第一个节点,初始化完成后将 page 标记为 hotPage :

id *autoreleaseNoPage(id obj)
{
    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) {
        // 正在空占位pool后将第二个pool入栈,
        // 或者正在将第一个objec入栈到空占位pool里面。
        // 并且在此操作之前,入栈一个POOL_BOUNDARY表示当前空池占位符呈现的pool
        pushExtraBoundary = true;
    }
    else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        // 入栈一个没有pool的对象,环境要求将进行无pool调试
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        // 正在入栈一个没有池的池,并且没有请求alloc-per-pool调试,
        // 安装并返回空池占位符。
        return setEmptyPoolPlaceholder();
    }

    // 新page并设置为hotPage
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    
    // 入栈 pool 边界对象
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    
    // 入栈 autorelease 对象
    return page->add(obj);
}

在忽略特殊调试模式的情况下,这个过程可以这样理解:

  1. 当添加第一个 autoreleasePool,这个时候会入栈一个 POOL_BOUNDARY 标识,这个时候并不会马上去创建一个新的 AutoreleasePoolPage, 而是通过 TLS 设置一个 EMPTY_POOL_PLACEHOLDER(空池占位符);
  2. 接下来如果入栈的是一个 autorelease 对象,那么这个时候就创建一个 AutoreleasePoolPage 并且设置为 hotPage,然后将 POOL_BOUNDARY 标识添加到栈上,然后再把 autorelease 对象地址添加到栈上。

Tips:
为什么上面不在第一步中直接就创建一个 AutoreleasePoolPage,而只是通过 TLS 设置一个空池占位符呢?空池占位符,顾名思义,有可能这个入栈的 autoreleasePool 是一个空池,它有可能一个 autorelease 对象都不会有,这个时候花费 4096 Bytes 内存空间去创建一个并不会使用的 AutoreleasePoolPage,就会带来性能和内存空间上的浪费,所以新添加一个 autoreleasePool 的时候,仅用 EMPTY_POOL_PLACEHOLDER 标识一下这是一个空池,等后续实际真正需要的时候再创建。

autoreleaseFullPage()

这个方法的逻辑比较简单,就是当前 hotPage 栈空间已满的时候,需要创建一个新 page 链接到链表的末端,并将新创建的 page 设置为 hotPage,再往 hotPage 里面添加 autorelease 对象。

id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    do {
        if (page->child) {
            page = page->child;
        } else {
            page = new AutoreleasePoolPage(page);
        }
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

流程

整个过程的流程大致就是:

  1. 添加新的 autoreleasePool,TLS 设置一个空池占位符;
  2. 如果接下来这个 autoreleasePool 并没有入栈 autorelease 对象就结束了,那么这个时候 TLS 清空空池占位符,一切恢复初始状态;如果接下来这个 autoreleasePool 入栈了一个 autorelease 对象(如果没有 hotPage,那么就创建一个新的 AutoreleasePoolPage,并设置为 hotPage),在 hotPage 里入栈一个 POOL_BOUNDARY 和这个 autorelease 对象;
  3. 后续还有 autorelease 对象。如果 hotPage 还有内存空间可用,那么就直接将 autorelease 对象地址添加到栈顶;如果 hotPage 没有可用内存空间了,那么就创建一个新的 AutoreleasePoolPage,并且作为上一个 page 的子节点,创建完以后将新的 page 设置为 hotPage,然后进行后续的 autorelease 对象入栈操作。
  4. 如果又添加了新的 autoreleasePool,那么就重复前面的 1、2 步骤。

用伪代码表示这个流程:

push()
   ├── autoreleaseFast(POOL_BOUNDARY) // 添加新 pool,没有 page,设置空池占位符
   │    └── autoreleaseNoPage(POOL_BOUNDARY)  
   │        └── setEmptyPoolPlaceholder
   ├── autoreleaseFast(obj) // 添加 autorealease 对象,创建新 page,入栈 POOL_BOUNDARY 和 对象
   │    └── autoreleaseNoPage(obj)
   │        ├── AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
   │        ├── setHotPage(page);
   │        ├── page->add(POOL_BOUNDARY);
   │        └── page->add(obj);
   ├── autoreleaseFast(obj1) // 添加 autorealease 对象
   │    ├── AutoreleasePoolPage *page = hotPage();
   │    └── page->add(obj1)
   ├── ...  // 添加 autorealease 对象
   ├── ...  
   ├── autoreleaseFast(objn) // 添加 autorealease 对象,page 空间满了,创建新 page
   │    └── autoreleaseFullPage()
   │        ├── AutoreleasePoolPage *page = new AutoreleasePoolPage(lastPage);
   │        ├── setHotPage(page);
   │        └── page->add(objn)
   ├── autoreleaseFast(objn1) // 添加 autorealease 对象
   │    ├── AutoreleasePoolPage *page = hotPage();
   │    └── page->add(objn1)
   │
   ...

objc_autoreleasePoolPop()

步骤

前面我们知道,objc_autoreleasePoolPush() 就是入栈了一个 POOL_BOUNDARY 在 autoreleasePool 的栈顶。再看一看 main() 函数里面,objc_autoreleasePoolPop() 就是把入栈的 POOL_BOUNDARY 出栈:

int main(int argc, const char *argv[]) {

    /* @autoreleasepool */
    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        // do something
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

看看 objc_autoreleasePoolPop() 的具体实现,调用了 AutoreleasePoolPage 类的 pop() 方法:

void *objc_autoreleasePoolPop(void)
{
    return AutoreleasePoolPage::pop();
}

static inline void pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;
    
    // 1
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        if (hotPage()) {
            pop(coldPage()->begin());
        } else {
            setHotPage(nil);
        }
        return;
    }
    
    // 2
    page = pageForPointer(token);
    stop = (id *)token;
    
    // 3
    page->releaseUntil(stop);
    
    // 4
    if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        } else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

pop() 方法中删除了一些正常逻辑无关的代码,这个过程大致可以分为4个步骤:

  1. 空占位池相关的处理;
  2. pageForPointer() 找到 token 所在的 AutoreleasePoolPage;
  3. releaseUntil() 释放 autorelease 对象;
  4. kill() 杀掉 child page,清理内存,避免线程堆栈中太多垃圾导致线程堆栈溢出;

pageForPointer

根据指针取 page 的地址。前面说了 AutoreleasePoolPage 的地址都是 4k 对齐的,所以这里将指针地址与 4096 进行模运算就得到了指针在 page 中的偏移地址,然后用指针地址减去这个偏移地址就得到了 page 地址。

    static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
    {
        AutoreleasePoolPage *result;
        uintptr_t offset = p % SIZE; // SIZE == 4096
        result = (AutoreleasePoolPage *)(p - offset);
       
        return result;
    }

releaseUntil()

这个过程是释放栈中 autorelease 对象,首先是找到链表尾部不为空的 page, 然后从栈顶 next 一直向栈低释放 autorelease 对象,直到找到 *stop 指针所在的位置。

void releaseUntil(id *stop) 
{
    while (this->next != stop) {
        // 1
        AutoreleasePoolPage *page = hotPage();
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }
        
        // 2
        page->unprotect();
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();
        
        // 3
        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }

    setHotPage(this);
}

kill()

这个方法比较简单,就是删除当前的 AutoreleasePoolPage 以及当前 AutoreleasePoolPage 的子页面,释放掉内存:

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);
}

顺便提一下 pop() 方法中第 4 步中杀掉当前页面的 child 页面的逻辑:

if (page->child) {
    if (page->lessThanHalfFull()) {
        page->child->kill();
    } else if (page->child->child) {
        page->child->child->kill();
    }
}
  • 这里为什么会有删除子页面这样一个逻辑?
    大致是这样的:如果当前页面的栈空间使用未超过一半,那么认为还有足够的空间使用,如果当前页面有子页面,那么一时半会儿还用不到后面的子页面,就直接 kill 掉把内存释放了;如果当前页面的栈空间使用超过一半了,那么就认为很快就会出现内存不够用的情况,如果当前页面有子页面的话,就保留这个一个子页面,子页面如果还有子页面,那么是要清理掉的,不用保留太多个页面,

  • 这些子页面是哪里来的呢?
    这些页面都是前面 autorelease 用到的页面, autorelease 对象在 release 后,但是用到的页面是还未被释放掉的,也就是页面还在双向链表中,所有这里要清理掉过多的页面来避免占用了过多的线程堆栈空间,避免线程堆栈溢出。