iOS 性能优化(自动释放池 & autorelease)

2,941 阅读10分钟

前言

上篇文章介绍了引用计数和弱引用的源码,这篇文章来探索一下自动释放池和autorelease的源码

autorelease

autorelease的作用是延迟释放对象,源码如下:

- (id)autorelease {
    return ((id)self)->rootAutorelease();
}
inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this; // 如果是 tagged pointer 类型,直接返回当前对象
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this; // 如果 prepareOptimizedReturn 是True,直接返回当前对象

    return rootAutorelease2(); // 前面的操作都没走,则调用 rootAutorelease2
}

拆解一下函数

  • prepareOptimizedReturn

prepareOptimizedReturn 是用于优化返回的,其实现如下

static ALWAYS_INLINE bool 
prepareOptimizedReturn(ReturnDisposition disposition)
{
    assert(getReturnDisposition() == ReturnAtPlus0); // 确保当前获取的 Disposition 为 false

    if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) { // 如果当前允许优化返回值
        if (disposition) setReturnDisposition(disposition); // 设置 ReturnDisposition 为 true
        return true;
    }

    return false;
}
  • rootAutorelease2

rootAutorelease2的实现很简单,就调用了AutoreleasePoolPage的autorelease方法,把对象放入到自动释放池

__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

自动释放池(Autoreleasepool)

首先来看下Autoreleasepool的数据结构

接着上面的AutoreleasePoolPage::autorelease((id)this)这个入口,从这里开始分析源码

static inline id autorelease(id obj)
{
    assert(obj); // 断言,对象不为空
    assert(!obj->isTaggedPointer()); // 断言,对象不为 TaggedPointer
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj); // 确保 dest 不存在或等于 EMPTY_POOL_PLACEHOLDER 或等于 obj
    return obj;
}
  • autoreleaseFast

接着往下走,查看autoreleaseFast的源码

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage(); // 获取当前最上层的 page
    if (page && !page->full()) { // 如果 page 存在且不是满的
        return page->add(obj); // 把对象添加到当前的 page
    } else if (page) {  // 如果 page 存在,但已经满了
        return autoreleaseFullPage(obj, page); // 调用 autoreleaseFullPage 方法
    } else { // 如果 page 不存在
        return autoreleaseNoPage(obj); // 调用 autoreleaseNoPage 方法
    }
}

可以看出自动释放池是由一个或多个AutoreleasePoolPage组成,接下来继续拆解函数

  • hotPage

hotPage方法是用来获取当前最上层的page

static inline AutoreleasePoolPage *hotPage() 
{
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key); // 通过 tls 查询可用 AutoreleasePoolPage 对象
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil; // 如果查询结果为 EMPTY_POOL_PLACEHOLDER,返回 nil
    if (result) result->fastcheck(); // 如果 result 不为空,则调用 fastcheck 方法,这里面做了检测线程的工作
    return result;
}

key 定义如下:

// Thread keys reserved by libc for our use.
#if defined(__PTK_FRAMEWORK_OBJC_KEY0)
#   define SUPPORT_DIRECT_THREAD_KEYS 1
#   define TLS_DIRECT_KEY        ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY0)
#   define SYNC_DATA_DIRECT_KEY  ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY1)
#   define SYNC_COUNT_DIRECT_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY2)
#   define AUTORELEASE_POOL_KEY  ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)
# if SUPPORT_RETURN_AUTORELEASE
#   define RETURN_DISPOSITION_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY4)
# endif
#else
#   define SUPPORT_DIRECT_THREAD_KEYS 0
#endif

static pthread_key_t const key = AUTORELEASE_POOL_KEY;
  • full

full 方法是用来检测当前的AutoreleasePoolPage是否满了,实现如下

id * end() {
    return (id *) ((uint8_t *)this+SIZE);
}

bool full() { 
    return next == end();
}

SIZE的定义如下

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

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
  • add

add 功能是把对象添加到AutoreleasePoolPage

id *add(id obj)
{
    assert(!full()); // 确保当前状态不满
    unprotect(); // 设置为读/写状态
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect(); // 设置为只读状态
    return ret;
}
  • autoreleaseFullPage

autoreleaseFullPage函数的工作是新建一个AutoreleasePoolPage并且把对象放进去

static __attribute__((noinline))
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 {
        if (page->child) page = page->child; // 如果 page->child 存在,则给 page 赋值 page->child
        else page = new AutoreleasePoolPage(page); // 如果不存在,则新建一个AutoreleasePoolPage
    } while (page->full());

    setHotPage(page); // 设置新建的 AutoreleasePoolPage 为 hotPage
    return page->add(obj); // 把对象添加到新建的 AutoreleasePoolPage
}

AutoreleasePoolPage的构造函数

AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
    : magic(), next(begin()), thread(pthread_self()),
      parent(newParent), child(nil), 
      depth(parent ? 1+parent->depth : 0), 
      hiwat(parent ? parent->hiwat : 0)
{ 
    if (parent) {
        parent->check();
        assert(!parent->child);
        parent->unprotect();
        parent->child = this;
        parent->protect();
    }
    protect();
}
  • setHotPage

通过tls去设置hotPage

static inline void setHotPage(AutoreleasePoolPage *page) 
{
    if (page) page->fastcheck();
    tls_set_direct(key, (void *)page);
}
  • autoreleaseNoPage

如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage,并调用 page->add(obj) 将对象添加到 AutoreleasePoolPage 的栈中

static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
{
    // "No page" could mean no pool has been pushed
    // or an empty placeholder pool has been pushed and has no contents yet
    assert(!hotPage());

    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) { // 如果当前有 poolPlaceholder
        // We are pushing a second pool over the empty placeholder pool
        // or pushing the first object into the empty placeholder pool.
        // Before doing that, push a pool boundary on behalf of the pool 
        // that is currently represented by the empty placeholder.
        pushExtraBoundary = true;
    }
    else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        // We are pushing an object with no pool in place, 
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug", 
                     pthread_self(), (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        // We are pushing a pool with no pool in place,
        // and alloc-per-pool debugging was not requested.
        // Install and return the empty pool placeholder.
        return setEmptyPoolPlaceholder(); // 设置占位符
    }


    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY); // 添加边界符,是一个nil,用来区别每个AutoreleasePoolPage的边界
    }
    
    // Push the requested object or pool.
    return page->add(obj);
}
  • haveEmptyPoolPlaceholder
static inline bool haveEmptyPoolPlaceholder()
{
    id *tls = (id *)tls_get_direct(key);
    return (tls == EMPTY_POOL_PLACEHOLDER);
}
  • setEmptyPoolPlaceholder
static inline id* setEmptyPoolPlaceholder()
{
    assert(tls_get_direct(key) == nil);
    tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
    return EMPTY_POOL_PLACEHOLDER;
}

看完了添加的过程,来看下AutoreleasePoolPage释放的过程。从上面的代码分析过程中我们可以得知AutoreleasePool本质是个双向链表,释放的过程就是从最后一张链表开始删除,逐级往上删,用下图表示:

来看下AutoreleasePoolPage::pop的源码

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}
static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
    // EMPTY_POOL_PLACEHOLDER:当自动释放池为空时的一个占位符
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) { // 如果传入的值为 EMPTY_POOL_PLACEHOLDER
        // Popping the top-level placeholder pool.
        if (hotPage()) { // 如果有 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); // 没有 hotPage, 即为空池, 设置 hotPage 为 nil
        }
        return;
    }

    page = pageForPointer(token); //根据 token 找到所在的 节点
    stop = (id *)token; //token 转换给 stop
    if (*stop != POOL_BOUNDARY) { //如果 stop 中存储的不是边界符
        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
            // 存在自动释放池的第一个节点存储的第一个对象不是边界符的情况, 有两种情况导致:
            // 1. 顶层池没释放, 但留下了第一个节点
            // 2. 没有自动释放池的 autorelease 对象
        } else {
            // Error. For bincompat purposes this is not 
            // fatal in executables built with old SDKs.
            // 非自动释放池的第一个节点, stop 存储的也不是边界符的情况
            return badPop(token); // 调用错误情况下的 badPop()
        }
    }

    if (PrintPoolHiwat) printHiwat(); //如果需要打印 hiwat, 则打印

    page->releaseUntil(stop); // 将自动释放池中 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) { // 如果 page 有 child 节点
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) { // 如果 page 已占用空间少于一半
            page->child->kill(); // kill 掉 page 的 child 节点
        }
        else if (page->child->child) { // 如果 page 的占用空间已经大于一半, 并且 page 的 child 节点有 child 节点
            page->child->child->kill(); // kill 掉 child 节点的 child 节点
        }
    }
}
  • pageForPointer

由于为 AutoreleasePoolPage 对象分配的地址都是按 4096 对齐的, 也就是说 AutoreleasePoolPage 对象所处的地址都是 4096 的倍数, 所以 token 转换为十进制数时, 对 4096 取余, 就能得到 token 地址对 AutoreleasePoolPage 对象地址的偏移量. 又因为 AutoreleasePoolPage 对象本身的大小是 56, 所以如果 token 对 4096 取余的结果如果小于 56 就是错误的, 此时会抛出异常. 否则 token 地址减去偏移量, 就是 AutoreleasePoolPage 对象的地址, 转换为 AutoreleasePoolPage * 类型的指针, 就是该 token 所处的 page 节点。

static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE; // 转换为十进制数的 p 余上 4096

    assert(offset >= sizeof(AutoreleasePoolPage)); // 如果余数小于 AutoreleasePoolPage 的大小则抛出异常

    result = (AutoreleasePoolPage *)(p - offset); // 十进制数 p 减掉刚刚得到的余数 offset, 结果转换为AutoreleasePoolPage * 类型指针
    result->fastcheck(); // 根据配置进行 check

    return result;
}
  • badPop

这个函数正常情况下是调用不到的, 只有使用旧 SDK 的时候有可能会发生. 一旦发生 badPop 时, 会记录下错误日志, 并销毁该自动释放池.

static void badPop(void *token)
{
    // Error. For bincompat purposes this is not 
    // fatal in executables built with old SDKs.
    // 对于旧的 SDK 来说, 这个错误并不是致命的
    if (DebugPoolAllocation || sdkIsAtLeast(10_12, 10_0, 10_0, 3_0, 2_0)) {
        // OBJC_DEBUG_POOL_ALLOCATION or new SDK. Bad pop is fatal.
        // 对于开启 pool 内存分配的 debug 模式, 以及最新 SDK 的情况, 调用到 badPop 是错误的
        _objc_fatal
            ("Invalid or prematurely-freed autorelease pool %p.", token);
    }

    // Old SDK. Bad pop is warned once.
    // 旧 SDK 下, Bad pop 会记录一次日志
    static bool complained = false;
    if (!complained) {
        complained = true;
        // 输出一系列信息到 crash log 里, 但不会触发 crash
        _objc_inform_now_and_on_crash
            ("Invalid or prematurely-freed autorelease pool %p. "
             "Set a breakpoint on objc_autoreleasePoolInvalid to debug. "
             "Proceeding anyway because the app is old "
             "(SDK version " SDK_FORMAT "). Memory errors are likely.",
                 token, FORMAT_SDK(sdkVersion()));
    }
    objc_autoreleasePoolInvalid(token); // 摧毁包含 token 的自动释放池
}
  • releaseUntil

自动释放池销毁对象中最重要的一环, 调用者是用 pageForPointer() 找到的, token 所在的 page 节点, 参数为 token. 这个函数主要操作流程就是, 从 hotPage 开始, 使用 next 指针遍历存储在节点里的 autorelease 对象列表, 对每个对象进行一次 release 操作, 并且把 next 指向的指针清空, 如果 hotPage 里面的对象全部清空, 则继续循环向前取 parent 并继续用 next 指针遍历 parent, 一直到 next 指针指向的地址为 token 为止. 因为 token 就在 this 里面, 所以这个时候的 hotPage 应该是 this.

void releaseUntil(id *stop) 
{
    // 这里没有使用递归, 防止发生栈溢出
    while (this->next != stop) { // 一直循环到 next 指针指向 stop 为止
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
        AutoreleasePoolPage *page = hotPage(); // 取出 hotPage

        while (page->empty()) { // 从节点 page 开始, 向前找到第一个非空节点
            page = page->parent; // page 非空的话, 就向 page 的 parent 节点查找
            setHotPage(page); // 把新的 page 节点设置为 HotPage
        }

        page->unprotect(); // 如果需要的话, 解除 page 的内存锁定
        id obj = *--page->next; // 先将 next 指针向前移位, 然后再取出移位后地址中的值
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // 将 next 指向的内存清空为SCRIBBLE
        page->protect(); // 如果需要的话, 设置内存锁定

        if (obj != POOL_BOUNDARY) { // 如果取出的对象不是边界符
            objc_release(obj); // 给取出来的对象进行一次 release 操作
        }
    }

    setHotPage(this); // 将本节点设置为 hotPage

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

自动释放池中需要 release 的对象都已操作完成, 此时 hotPage 之后的 page 节点都已经清空了, 需要把这些节点的内存都回收, 操作方案就是从最后一个节点, 遍历到调用者节点, 挨个回收.

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; // child 置空
            page->protect(); // 如果需要的话, 设置内存锁定
        }
        delete deathptr; // 回收刚刚保留的节点, 重载 delete, 内部调用 free
    } while (deathptr != this);
}

参考文章

从源码角度看苹果是如何实现 autorelease 和 autoreleasepool 的
AutoreleasePool 的实现机制 (四)