iOS内存管理二:自动释放池autoreleasepool

2,863 阅读13分钟

在上一篇文章中,详细分析了IOS内存管理的内存布局、内存管理方案、引用计数等内容,本篇文章将继续上篇文章的内容探索自动释放池autoreleasepool的相关知识。

1、autoreleasepool初探

熟悉OC开发的都知道,在main函数中就有@autoreleasepool这样一个东西,其实这就是自动释放池。那么@autoreleasepool的底层实现是什么样的呢?我们在命令行中使用 clang -rewrite-objc main.m -o main.cpp 让编译器重新改写这个文件,讲得到一个main.cpp文件,打开该文件,找到其中的main函数。

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

    }
    return 0;
}

我们可以看到@autoreleasepool转化成了__AtAutoreleasePool这样一个结构体,那么意味着@autoreleasepool的本质就是__AtAutoreleasePool结构体。

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

这个结构体会在初始化时调用objc_autoreleasePoolPush() 方法,会在析构时调用 objc_autoreleasePoolPop 方法。

这就说明了main函数在实际工作的时候是这样的:

int main(int argc, const char *argv[])
{
    void * atautoreleasepoolobj = objc_autoreleasePoolPush();

    // do whatever you want

    objc_autoreleasePoolPop(atautoreleasepoolobj);
    return 0;
}

似乎一切都是围绕着objc_autoreleasePoolPush()objc_autoreleasePoolPop这两个方法展开。那么我们来看下这两个方法的源码实现:

void *
objc_autoreleasePoolPush(void)
{
    // 调用了AutoreleasePoolPage中的push方法
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    // 调用了AutoreleasePoolPage中的pop方法
    AutoreleasePoolPage::pop(ctxt);
}

上面的两个方法看上去是对AutoreleasePoolPage对应静态方法pushpop的封装。

2、AutoreleasePoolPage

在runtime中的源码(objc4-756.2版本)中找到了一段注释,这段注释对我们理解AutoreleasePoolPage的底层结构会有所帮助。

  • 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 所对应的 POOL_BOUNDARY 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被 release。
  • 这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除。
  • Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。

从上面这段注释中我们可以知道自动释放池是一种栈的结构,遵循先进后出的原则,每一个自动释放池都是由一系列的AutoreleasePoolPage组成的,而AutoreleasePoolPage是以双向链表的形式连接起来。

2.1、AutoreleasePoolPage结构

来看一下AutoreleasePoolPage的代码定义(只列出了关键代码,部分代码省略)。

class AutoreleasePoolPage 
{
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)
#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    // AutoreleasePoolPage的大小,通过宏定义,可以看到是4096字节
    static size_t const SIZE =
 #if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
 #else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
 #endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;//16字节
    id *next;//8字节
    pthread_t const thread;//8字节
    AutoreleasePoolPage * const parent;//8字节
    AutoreleasePoolPage *child;//8字节
    uint32_t const depth;//4字节
    uint32_t hiwat;//4字节
}
  • magic:用来校验AutoreleasePoolPage的结构是否完整。
  • *next:next指向的是下一个AutoreleasePoolPage中下一个为空的内存地址(新来的对象会存储到next处),初始化时指向begin()
  • thread:保存了当前页所在的线程(一个AutoreleasePoolPage属于一个线程,一个线程中可以有多个AutoreleasePoolPage)。
  • *parent:指向父节点,第一个parent节点为nil
  • *child:指向子节点,最后一个child节点为nil
  • depth:代表深度,从0开始,递增+1。
  • hiwat:代表high water Mark最大入栈数。
  • SIZEAutoreleasePoolPage的大小,值为PAGE_MAX_SIZE,4096个字节。
  • POOL_BOUNDARY:只是nil的别名。在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个POOL_SENTINEL push到自动释放池的栈顶,并且返回这个POOL_SENTINEL自动释放池边界。而当方法 objc_autoreleasePoolPop调用时,就会向自动释放池中的对象发送release消息,直到第一个 POOL_SENTINEL

AutoreleasePoolPage中的第一个对象是存储在next后面,那么就形成如下图所示这样一个结构。

其中的56个字节存储的AutoreleasePoolPage的成员变量,其他的区域存储加载到自动释放池的对象。 当next==begin()时表示AutoreleasePoolPage为空,当next==end()的时表示AutoreleasePoolPage已满。

2.2、AutoreleasePoolPage容量

在上一个小节的内容中我们分析了AutoreleasePoolPage的结构,了解到每一个AutoreleasePoolPage的大小是4096字节,其中56字节用于存储成员变量,剩下的区域存储加载到自动释放池的对象,那么似乎答案呼之欲出,一个AutoreleasePoolPage可以存储(4096-56)/8=505个对象。但是有一个注意的点,第一个page存放的需要释放的对象的容量应该是504个,因为在创建page的时候会在next的位置插入1POOL_SENTINEL

2.3、push方法

通过前面小节的分析,我们知道objc_autoreleasePoolPush的本质就是调用push方法。我们先来看下push方法的源码。

static inline void *push() 
{
    id *dest;
    if (slowpath(DebugPoolAllocation)) {
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}

push方法中实际上调用的是autoreleaseFast方法,并且首先将一个POOL_BOUNDARY对象插入到栈顶。slowpath表示小概率发生。

2.3.1、autoreleaseFast方法

如下是autoreleaseFast方法的源码

 static inline id *autoreleaseFast(id obj)
{
    // hotPage就是当前正在使用的AutoreleasePoolPage
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // 有hotPage且hotPage不满,将对象添加到hotPage中
        return page->add(obj);
    } else if (page) {
        // 有hotPage但是hotPage已满
        // 使用autoreleaseFullPage初始化一个新页,并将对象添加到新的AutoreleasePoolPage中
        return autoreleaseFullPage(obj, page);
    } else {
        // 无hotPage
        // 使用autoreleaseNoPage创建一个hotPage,并将对象添加到新创建的page中
        return autoreleaseNoPage(obj);
    }
}

autoreleaseFast方法的代码很简单,只要是三个判断分支。

  1. 如果有hotPage且没有满,则调用add方法将对象添加到hotPage中。否则执行第2步。
  2. 如果有hotPage但是已经满了,则调用autoreleaseFullPage方法初始化一个新页,并将对象添加到新的AutoreleasePoolPage中。否则执行第3步。
  3. 如果没有hotPage,则调用autoreleaseNoPage方法创建一个hotPage,并将对象添加到新创建的page

hotPage 可以理解为当前正在使用的 AutoreleasePoolPage。

2.3.2、add 添加对象

add方法将对象添加到AutoreleasePoolPage中。

id *add(id obj)
{
    ASSERT(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;//将obj存放在next处,并将next指向下一个位置
    protect();
    return ret;
}

这个方法其实就是一个压栈操作,将对象添加到AutoreleasePoolPage中,然后移动栈顶指针。

2.3.3、autoreleaseFullPage

autoreleaseFullPage方法会重新开辟一个新的AutoreleasePoolPage页,并将对象添加到其中。

id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // The hot page is full. 
    // Step to the next non-full page, adding a new page if necessary.
    // Then add the object to that page.
    assert(page == hotPage());
    assert(page->full()  ||  DebugPoolAllocation);

    do {
        // 如果page->child不为空,那么使用page->child
        if (page->child) page = page->child;
        // 否则的话,初始化一个新的AutoreleasePoolPage
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    // 将找到的合适的page设置成hotPage
    setHotPage(page);
    // 将对象添加到hotPage中
    return page->add(obj);
}

遍历找到未满的的page,如果没有找到则初始化一个新的page,并将page设置为hotPage,同时将对象添加到这个page中。

2.3.4、autoreleaseNoPage

如果当前内存中不存在hotPage,就会调用autoreleaseNoPage方法初始化一个AutoreleasePoolPage

id *autoreleaseNoPage(id obj)
{
    // Install the first page.
    // 初始化一个AutoreleasePoolPage
    // 当前内存中不存在AutoreleasePoolPage,则从头开始构建AutoreleasePoolPage,也就是其parent为nil
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    // 将初始化的AutoreleasePoolPage设置成hotPage
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool.
    // 添加一个边界对象(nil)
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    
    // Push the requested object or pool.
    // 将对象添加到AutoreleasePoolPage中
    return page->add(obj);
}

当前内存中不存在AutoreleasePoolPage,则从头开始构建AutoreleasePoolPage,也就是其parentnil。初始化之后,将当前页标记为hotPage,然后会先向这个page中添加一个POOL_SENTINEL 对象,来确保在pop调用的时候,不会出现异常。最后,将对象添加到自动释放池中。

2.4、pop方法

上面小节我们探索了objc_autoreleasePoolPush,下面我们看看objc_autoreleasePoolPopobjc_autoreleasePoolPop的本质就是调用pop方法。

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;

    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        // Popping the top-level placeholder pool.
        if (hotPage()) {
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            pop(coldPage()->begin());
        } else {
            // Pool was never used. Clear the placeholder.
            setHotPage(nil);
        }
        return;
    }

    page = pageForPointer(token);
    stop = (id *)token;
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
            // Start of coldest page may correctly not be POOL_BOUNDARY:
            // 1. top-level pool is popped, leaving the cold page in place
            // 2. an object is autoreleased with no pool
        } else {
            // Error. For bincompat purposes this is not 
            // fatal in executables built with old SDKs.
            return badPop(token);
        }
    }

    if (PrintPoolHiwat) printHiwat();

    page->releaseUntil(stop);

    // memory: delete empty children
    if (DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top) 
        // when debugging missing autorelease pools
        page->kill();
        setHotPage(nil);
    } 
    else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

上面方法做了如下几件事:

  1. 调用pageForPointer获取当前token所在的page。
  2. 调用releaseUntil方法释放栈中的对象,直到stop
  3. 调用childkill方法。

2.4.1、pageForPointer找到page

pageForPointer方法主要是通过通过内存地址的操作,获取当前token所在页的首地址。

static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE;

    ASSERT(offset >= sizeof(AutoreleasePoolPage));

    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();

    return result;
}

将指针与页面的大小(4096)取模,可以得到当前指针的偏移量。然后将指针的地址减偏移量便可以得到首地址。

2.4.2、releaseUntil释放对象

void releaseUntil(id *stop) 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    
    // 释放AutoreleasePoolPage中的对象,直到next指向stop
    while (this->next != stop) {
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
        // hotPage可以理解为当前正在使用的page
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it
        // 如果page为空的话,将page指向上一个page
        // 注释写到猜测这里可以使用if,我感觉也可以使用if
        // 因为根据AutoreleasePoolPage的结构,理论上不可能存在连续两个page都为空
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        // obj = page->next; page->next--;
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        // POOL_BOUNDARY为nil,是哨兵对象
        if (obj != POOL_BOUNDARY) {
            // 释放obj对象
            objc_release(obj);
        }
    }

    // 重新设置hotPage
    setHotPage(this);

#if DEBUG
    // we expect any children to be completely empty
    for (AutoreleasePoolPage *page = child; page; page = page->child) {
        assert(page->empty());
    }
#endif
}

因为AutoreleasePool实际上就是由AutoreleasePoolPage组成的双向链表,因此,*stop可能不是在最新的AutoreleasePoolPage中,即hotPage,这时需要从hotPage开始,一直释放,直到stop,中间所经过的所有AutoreleasePoolPage里面的对象都要释放。
对象的释放objc_release方法请移步前面的文章iOS内存管理一:Tagged Pointer&引用计数

2.4.3、kill方法

kill方法删除双向链表中的每一个page

void kill() 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    AutoreleasePoolPage *page = this;
    // 找到链表最末尾的page
    while (page->child) page = page->child;

    AutoreleasePoolPage *deathptr;
    // 循环删除每一个page
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    } while (deathptr != this);
}

3、自动释放池和线程

官方文档Using Autorelease Pool Blocks中关于自动释放池和线程的关系有如下一段描述。

Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.
If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows. If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.

翻译成中文如下:

应用程序中的每个线程都维护自己的自动释放池块堆栈。如果您正在编写一个仅限基础的程序,或者正在分离一个线程,那么您需要创建自己的自动释放池块。
如果您的应用程序或线程是长生命周期的,并且可能会生成大量的自动释放对象,那么您应该使用自动释放池块(如在主线程上使用AppKit和UIKit);否则,自动释放的对象会累积,内存占用会增加。如果分离的线程不进行Cocoa调用,则不需要使用自动释放池块。

从上面这段秒速我们可以知道自动释放池和线程是紧密相关的,每一个自动释放池只对应一个线程。

4、AutoreleasePool和RunLoop

一般很少会将自动释放池和RunLoop联系起来,但是如果打印[NSRunLoop currentRunLoop]结果中会发现和自动释放池相关的回调。

<CFRunLoopObserver 0x6000024246e0 [0x7fff8062ce20]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
<CFRunLoopObserver 0x600002424640 [0x7fff8062ce20]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}

即App启动后,苹果会给RunLoop注册很多个observers,其中有两个是跟自动释放池相关的,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。\

  • 第一个observer监听的是activities=0x1(kCFRunLoopEntry),也就是在即将进入loop时,其回调会调用_objc_autoreleasePoolPush() 创建自动释放池;
  • 第二个observer监听的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit), 即监听的是准备进入睡眠和即将退出loop两个事件。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用_objc_autoreleasePoolPop()释放旧的释放池,并调用_objc_autoreleasePoolPush() 创建新建一个新的,用来装载被唤醒后要处理的事件对象;在最后即将退出loop时则会 _objc_autoreleasePoolPop()释放池子。

5、总结

  1. 自动释放池是由AutoreleasePoolPage以双向链表的方式实现的。
  2. 当对象调用autorelease方法时,会将对象加入AutoreleasePoolPage的栈中。
  3. 调用AutoreleasePoolPage::pop方法会向栈中的对象发送release消息。
  4. 自动释放池和线程是紧密相关的,每一个自动释放池只对应一个线程

参考资料