[iOS] AutoreleasePool实现原理

AutoreleasePool自动释放池,是 OC 中的一种内存自动回收机制,可以将加入自动释放池中的对象的release 时机延迟。当自动释放池作用域结束时,将池中的对象统一发送一次 release 消息,当对象的引用计数为零时,对象就会被释放。

我们这次来探究一下AutoreleasePool自动释放池的实现原理!

1. AutoreleasePool底层结构

在 main.m 编写如下函数:

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

通过clang -rewrite-objc main.m 指令,将 main.m 转化为main.cpp文件,自动释放池部分代码如下:

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
  // 构造函数
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  // 析构函数
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  // 成员变量
  void * atautoreleasepoolobj;
};

#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    }
    return 0;
}

上面的代码很清晰,自动释放池构造时,会调用objc_autoreleasePoolPush()函数,销毁时会调用objc_autoreleasePoolPop()函数。

对应 objc 中的源码实现为:

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

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


void *
_objc_autoreleasePoolPush(void)
{
    return objc_autoreleasePoolPush();
}

void
_objc_autoreleasePoolPop(void *ctxt)
{
    objc_autoreleasePoolPop(ctxt);
}

可以看到,自动释放池构造时调用AutoreleasePoolPage::push()函数,析构时调用 AutoreleasePoolPage::pop(ctxt);函数。下面也会从这两个函数为切入点进行分析。

AutoreleasePoolPage类实现的上方有一段注释,翻译如下:

  • 一个线程的自动释放池就是一个存放对象指针的栈(AutoreleasePool的结构是由AutoreleasePoolPage作为节点构成的双向链表,而每个AutoreleasePoolPage里面有一个存放对象指针的)。
  • 栈里面的每个指针要么是等待autorelease的对象,要么是POOL_BOUNDARY自动释放池边界(实际为#define POOL_BOUNDARY nil,同时也是 next 的指向)。一个pool token是指向 POOL_BOUNDARY 的指针。
    -AutoreleasePoolPage 会根据需要进行动态添加和删除。
  • hotPage 保存在当前线程中,当有新的autorelease 对象添加进自动释放池时会被添加到hotPage

先大概理解一下,下面继续看下AutoreleasePoolPage类:

//************宏定义************
#define PAGE_MIN_SIZE           PAGE_SIZE
#define PAGE_SIZE               I386_PGBYTES
#define I386_PGBYTES            4096            /* bytes per 80386 page */

//************类定义************
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    friend struct thread_data_t;

public:
    //页的大小
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif
... 方法实现先省略
}

从源码中可以看出AutoreleasePoolPage 是私有继承自AutoreleasePoolPageData的类,并且页的大小为 4096字节,保存的 autorelease 对象的指针,每个指针占 8 个字节(后面会进行验证)。

我们继续看下AutoreleasePoolPageData,是一个结构体:

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
    magic_t const magic; //用来校验AutoreleasePoolPage的结构是否完整
    __unsafe_unretained id *next; // 作为游标指向栈顶最新 add 进来的 autorelease 对象的下一个位置
    pthread_t const thread; // AutoreleasePool是按线程一一对应的,thread 是自动释放池所处的线程
    AutoreleasePoolPage * const parent; // 指向前面的节点
    AutoreleasePoolPage *child;  // 指向后面的节点
    uint32_t const depth; // 标记每个结点在链表中的深度,第一个节点为 0,后面依次递增
    uint32_t hiwat;

      // 构造函数
    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

AutoreleasePoolPageData结构体中可以看出,其包含构成双向链表的两个指针parentchild,所以说明自动释放池确实是一个双向链表结构。

AutoreleasePoolPageData结构体占用的内存大小为56个字节(这个点在下面会体现到):
-magic的类型是 magic_t结构体,成员变量只有uint32_t m[4];,占用 4 * 4 = 16字节

  • next、thread 、parent、child均占8字节,即4 * 8 = 32字节
  • 属性depth、hiwat类型为uint32_t,实际类型是unsigned int类型,均占4字节(即2 * 4 = 8字节)

大概是下图这样的结构:

image.png

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

大概了解了AutoreleasePool的结构之后,我们就从objc_autoreleasePoolPush方法开始进行分析。

2. objc_autoreleasePoolPush源码分析

objc_autoreleasePoolPush方法,内部是调用了AutoreleasePoolPage::push()方法,源码如下:

//入栈
static inline void *push() 
{
    id *dest;
    // OPTION( DebugPoolAllocation, OBJC_DEBUG_POOL_ALLOCATION,
    // "halt when autorelease pools are popped out of order, 
    // and allow heap debuggers to track autorelease pools")
    // 当自动释放池弹出顺序时停止,并允许堆调试器跟踪自动释放池

    if (slowpath(DebugPoolAllocation)) {
        // Each autorelease pool starts on a new pool page.自动释放池从新池页面开始
        // 创建一个新的自动释放池
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        // 压栈一个POOL_BOUNDARY,即压栈哨兵
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}

从源码中可以看出,如果自动释放池不存在,则构建一个新的pagepush 函数的作用可以理解为,调用AutoreleasePoolPage::push在当前线程的存储空间保存一个 EMPTY_POOL_PLACEHOLDER

autoreleaseFast 函数比autoreleaseNewPage多了一个判断page还没有满时就直接添加 objpage中的逻辑,剩下的调用autoreleaseFullPageautoreleaseNoPage 是一样的。

2.1 autoreleaseNewPage函数

autoreleaseNewPage函数源码如下:

static __attribute__((noinline))
    id *autoreleaseNewPage(id obj)
    {
        // 获取当前操作的page
        AutoreleasePoolPage *page = hotPage();
        // 如果存在则压栈对象
        if (page) return autoreleaseFullPage(obj, page);
        // 如果不存在则创建页
        else return autoreleaseNoPage(obj);
    }
2.2 hotPage

获取当前的页,hotPage是在线程中存储的,代码如下:

static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        // 如果是一个空池,则返回nil,否则,返回当前线程的自动释放池
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }
2.3 autoreleaseFullPage

page 满了的时候,就会调用autoreleaseFullPage方法,主要是构建新的AutoreleasePoolPage,添加进双向链表中,并把obj添加进去,代码如下:

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.
    // 如果 hotpage 满了,转到下一个未满的 page,如果不存在的话添加一个新的 page。  
    // 然后把 object 添加到新 page 里。
    
    // page 必须是 hotPage
    ASSERT(page == hotPage());
    // page 满了,或者自动释放池按顺序弹出时暂停,并允许堆调试器跟踪自动释放池
    
    // OPTION( DebugPoolAllocation,
    //         OBJC_DEBUG_POOL_ALLOCATION,
    //         "halt when autorelease pools are popped out of order,
    //          and allow heap debuggers to track autorelease pools")
    // 自动释放池按顺序弹出时暂停,并允许堆调试器跟踪自动释放池
    
    ASSERT(page->full()  ||  DebugPoolAllocation);

    // do while 循环里面分为两种情况
    // 1. 沿着 child 往后走,如果能找到一个非满的 page,则可以把 obj 放进去
    // 2. 如果 child 不存在或者所有的 child 都满了,
    //    则构建一个新的 AutoreleasePoolPage 并拼接在 AutoreleasePool 的双向链表中,
    //    并把 obj 添加进新 page 里面
    do {
        if (page->child) page = page->child;
        // 构造方法时传进的page的child会指向新构建的page,也就是添加新构建的page到双向链表中
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    // 设置 page 为 hotPage
    setHotPage(page);
    
    // 把 obj 添加进 page 里面,返回值是 next 之前指向的位置 (objc_object **)
    return page->add(obj);
}

其中 page->add(obj)是将obj 压入栈中:

id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }
2.4 autoreleaseNoPage

autoreleaseNoPage创建新页的方法,代码如下:

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
    // "No page" 可能意味着没有构建任何池,或者只有一个 EMPTY_POOL_PLACEHOLDER 占位
    
    // hotPage 不存在,否则执行断言
    ASSERT(!hotPage());

    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) {
        // 如果线程里面存储的是 EMPTY_POOL_PLACEHOLDER
        
        // 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) {
        // OPTION( DebugMissingPools, OBJC_DEBUG_MISSING_POOLS,
        // "warn about autorelease with no pool in place, which may be a leak")
        // 警告在没有自动释放池的情况下进行 autorelease,
        // 这可能导致内存泄漏(可能是因为没有释放池,然后对象缺少一次 objc_release 执行,导致内存泄漏)
        // 如果 obj 不为 nil 并且 DebugMissingPools。
        
        // We are pushing an object with no pool in place,
        // and no-pool debugging was requested by environment.
        // 我们正在没有自动释放池的情况下把一个对象往池里推,
        // 并且打开了 environment 的 no-pool debugging,此时会在控制台给一个提示信息。
        
        // 线程内连 EMPTY_POOL_PLACEHOLDER 都没有存储,并且如果 DebugMissingPools 打开了,则控制台输出如下信息
        _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug", 
                     objc_thread_self(), (void*)obj, object_getClassName(obj));
                     
        // obj 不为 nil,并且线程内连 EMPTY_POOL_PLACEHOLDER 都没有存储
        // 执行 objc_autoreleaseNoPool,且它是个 hook 函数             
        objc_autoreleaseNoPool(obj);
        
        // 返回 nil
        return nil;
    }
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        // OPTION( DebugPoolAllocation, OBJC_DEBUG_POOL_ALLOCATION, 
        //         "halt when autorelease pools are popped out of order, 
        // and allow heap debuggers to track autorelease pools")
        // 当自动释放池顺序弹出时暂停,并允许堆调试器跟踪自动释放池
        // 如果 obj 为空,并且没有打开 DebugPoolAllocation
        
        // We are pushing a pool with no pool in place,
        // and alloc-per-pool debugging was not requested.
        // 在没有池的情况下,我们设置一个空池占位,并且不要求为池分配空间和调试。(空池占位只是一个 ((id*)1))
        
        // Install and return the empty pool placeholder.
        
        // 根据 key 在当前线程的存储空间内保存 EMPTY_POOL_PLACEHOLDER 占位
        return setEmptyPoolPlaceholder();
    }

    // We are pushing an object or a non-placeholder'd pool.
    // 构建非占位的池

    // Install the first page.
    // 构建自动释放池的第一个真正意义的 page
    
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    
    // 设置为 hotPage
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool.
    // 代表先前占位符的池推边界。
    
    // 如果之前有一个 EMPTY_POOL_PLACEHOLDER 在当前线程的存储空间里面占位的话
    if (pushExtraBoundary) {
        // 池边界前进一步
        
        // 可以理解为把 next 指针往前推进了一步,并在 next 之前的指向下放了一个 nil 
        page->add(POOL_BOUNDARY);
    }
    
    // Push the requested object or pool.
    // 把 objc 放进自动释放池
    return page->add(obj);
}

这个方法主要是在线程中创建一个EMPTY_POOL_PLACEHOLDER占位,然后创建自动释放池的第一页,并且将哨兵对象压进栈中。

其中AutoreleasePoolPage的创建是通过其构造方法实现的,它的构造方法其实是AutoreleasePoolPageData的构造方法:

//**********AutoreleasePoolPage构造方法**********
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),//开始存储的位置
                                objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
                                newParent,
                                newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
                                newParent ? newParent->hiwat : 0)
{ 
    if (parent) {
        parent->check();
        ASSERT(!parent->child);
        parent->unprotect();
        //this 表示 新建页面,将当前页面的子节点 赋值为新建页面
        parent->child = this;
        parent->protect();
    }
    protect();
}

//**********AutoreleasePoolPageData初始化方法**********
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }

上面 begin()表示压栈的位置,即下一个要释放对象的压栈地址,其实就是页的首地址 + 56,这个56其实就是结构体AutoreleasePoolPageData本身所占用的内存大小,在上面我们已经计算过了。

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

主要是判断当前页是否满了,如果没满,则直接添加,如果满了则开辟新页,如果当前页都没有,再进入autoreleaseNoPage的流程。

2.3 自动释放池内存结构

在分析了objc_autoreleasePoolPush之后,我们对于自动释放池有了个大概的认识,现在我们通过代码去验证下他的结构:
由于在ARC模式下,是无法手动调用autorelease,所以将Demo切换至MRC模式(Build Settings -> Objectice-C Automatic Reference Counting设置为NO):

image.png

定义如下代码:

//************打印自动释放池结构************
extern void _objc_autoreleasePoolPrint(void);

//************运行代码************
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //循环创建对象,并加入自动释放池
        for (int i = 0; i < 5; i++) {
             NSObject *objc = [[NSObject alloc] sutorelease];
        }
        //调用
        _objc_autoreleasePoolPrint();
    }
}

运行结果如下,发现是 6 个,但是我们实际上创建了 5个对象啊,其中的POOL表示哨兵,即边界,其目的是为了防止越界,也就是我们在上面autoreleaseNoPage方法中添加的POOL_BOUNDARY:

image.jpeg

从上图中我们看到,page 的首地址是0x100817000 和 哨兵对象的地址0x100817038相差0x38 ,转换成十进制刚好是56,也就是AutoreleasePoolPage本身所占用的大小,想想上面的 begin()方法,也是从首地址 + 56 开始存储的。

在一开始我们还提到 page的大小为 4096 字节,自身占用了56个字节,哨兵对象占用了8个字节,所以除去哨兵对象还能存储 4032 / 8 = 504个对象,我们将上述的测试代码的循环次数数据改为 505,其内存结构如下,发现第一页满了,存储了504个要释放的对象,第二页只存储了一个 ,说明是对的:

image.png

我们将数据改为505 + 506,来验证第二页是否也是存储 505个对象:

image.jpeg

可以发现,第一页存储了504 个,第二页存储了 505个,第三页存储了2 个。

所以通过上述测试,可以得出以下结论:

  • 第一页可以存放 504 个对象,且只有第一页有哨兵对象,当第一页满了,就会开辟新的页
  • 第二页开始,最多可以存放505个对象
  • 一页的大小 = 505 * 8 + 56 = 4096 字节

自动释放池结构如下图所示:


image.png
2.4. autorelease底层分析

在 demo 中,我们通过 autorelease方法,在MRC的模式下,将对象压栈到自动释放池,会调用objc_autorelease方法,源码如下:

__attribute__((aligned(16), flatten, noinline))
id
objc_autorelease(id obj)
{
    //如果不是对象,则直接返回
    if (!obj) return obj;
    //如果是小对象,也直接返回
    if (obj->isTaggedPointer()) return obj;
    return obj->autorelease();
}

进入对象的autorelease实现:

👇
inline id 
objc_object::autorelease()
{
    ASSERT(!isTaggedPointer());
    //判断是否是自定义类
    if (fastpath(!ISA()->hasCustomRR())) {
        return rootAutorelease();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease));
}
👇
inline id 
objc_object::rootAutorelease()
{
    //如果是小对象,直接返回
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}
👇
__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    ASSERT(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}
👇
static inline id autorelease(id obj)
{
    ASSERT(obj);
    ASSERT(!obj->isTaggedPointer());
    //autoreleaseFast 压栈操作
    id *dest __unused = autoreleaseFast(obj);
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

从这里看出,无论是压栈哨兵对象,还是普通对象,都会来到autoreleaseFast方法,只是区别标识不同。

2.5 总结

autoreleaseobjc_autoreleasePush的整体流程如下图所示:

image.png

3. objc_autoreleasePoolPop 源码分析

objc_autoreleasePoolPop方法中有个参数,在clang分析时,发现传入的参数是push压栈后返回的对象,即ctxt,其目的是避免出栈混乱,防止将别的对象出栈:

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

    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        // Popping the top-level placeholder pool.
        // 弹出顶级 EMPTY_POOL_PLACEHOLDER 占位符池
        
        // 取出 hotPage
        page = hotPage();
        if (!page) {
            // 如果 hotPage 不存在,则表示目前就一 EMPTY_POOL_PLACEHOLDER,说明池还没有使用过
            // Pool was never used. Clear the placeholder.
            // Pool 从未使用过。清除占位符。
            return setHotPage(nil);
        }
        // Pool was used. Pop its contents normally.
        // Pool 是使用过了。正常弹出其内容。
        // Pool pages remain allocated for re-use as usual.
        // Pool pages 保持分配以照常使用.
        
        // 第一个 page
        page = coldPage();
        // 把第一个 page 的 begin 赋值给 token
        token = page->begin();
    } else {
        // token 转为 page 
        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);
        }
    }
    
    // allowDebug 为 true
    if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
        return popPageDebug(token, page, stop);
    }
    
    // 释放对象删除 page
    return popPage<false>(token, page, stop);
}


进入popPage源码,其中传入的allowDebugfalse,则通过releaseUntil出栈当前页stop位置之前的所有对象,即向栈中的对象发送release消息,直到遇到传入的哨兵对象:

//出栈页面
template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    if (allowDebug && PrintPoolHiwat) printHiwat();
    //出栈当前操作页面对象
    page->releaseUntil(stop);

    // memory: delete empty children 删除空子项
    if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        //调试期间删除每个特殊情况下的所有池
        //获取当前页面的父节点
        AutoreleasePoolPage *parent = page->parent;
        //将当前页面杀掉
        page->kill();
        //设置操作页面为父节点页面
        setHotPage(parent);
    }
    else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top)
        // when debugging missing autorelease pools
        //特殊情况:调试丢失的自动释放池时删除pop(top)的所有内容
        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();
        }
    }
}

进入releaseUntil实现,主要是通过循环遍历,判断对象是否等于stop,其目的是释放stop之前的所有的对象:

    void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        // 循环从 next 开始,一直后退,直到 next 到达 stop
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            
            // 取得当前的 AutoreleasePoolPage
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            // fixme 我认为 “while” 可以是 “if”,但我无法证明
            // 我觉得也是可以用 if 代替 while
            // 一个 page 满了会生成一个新的 page 并链接为下一个 page,
            // 所以从第一个 page 开始到 hotPage 的前一个page,应该都是满的
            
            // 如果当前 page 已经空了,则往后退一步,把前一个 AutoreleasePoolPage 作为 hotPage
            while (page->empty()) {
                // 当前 page 已经空了,还没到 stop,
                // 往后走 
                page = page->parent;
                // 把 page 作为 hotPage
                setHotPage(page);
            }
            
            // 可读可写
            page->unprotect();
            
            // next 后移一步,并用解引用符取出 objc_object * 赋值给 obj
            id obj = *--page->next;
            
            // 把 page->next 开始的 sizeof(*page->next) 个字节置为 SCRIBBLE
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            
            // 只可读
            page->protect();
            
            // 如果 obj 不为 nil,则执行 objc_release 操作
            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        // 这里还是把 this 作为 hotPage,
        // 可能从 stop 所在的 page 开始到 hotPage 这些 page 本来存放自动释放对象的位置都放的是 SCRIBBLE
        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        // 保证从当前 page 的 child 开始,向后都是空 page
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }


从最前面的page开始一直向后移动直到到达stop 所在的 page,并把经过的page里保存的对象都执行一次 objc_release操作,把之前每个存放 objc_object ** 的空间都置为 SCRIBBLE,每个pagenext 都指向了该 pagebegin

release做的事情是遍历释放保存的自动释放对象,而kill 做的事情是遍历对 AutoreleasePoolPage 执行 delete操作:

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 开始一直沿着 child 链往前走,直到 AutoreleasePool 的双向链表的最后一个 page
    while (page->child) page = page->child;

    // 临时变量(死亡指针)
    AutoreleasePoolPage *deathptr;
    
    // 是 do while 循环,所以会至少进行一次 delete,
    // 即当前 page 也会被执行 delete(不同与上面的 release 操作,入参 stop 并不会执行 objc_release 操作)
    do {
        // 要执行 delete 的 page
        deathptr = page;
        
        // 记录前一个 page
        page = page->parent;
        
        // 如果当前 page 的 parent 存在的话,要把这个 parent 的 child 置为 nil
        // 这个是链表算法题的经典操作
        if (page) {
            // 可读可写
            page->unprotect();
            
            // child 置为 nil
            page->child = nil;
            
            // 可写
            page->protect();
        }
        
        // delete page
        delete deathptr;
    } while (deathptr != this);
}


从当前的page 开始,一直根据 child 链向前走直到 child为空,把经过的page全部执行delete 操作(包括当前page)。

pthread_exit线程退出时,触发了_pthread_tsd_cleanup,触发AutoreleasePoolPagetls_dealloc(void*),然后回收autorelease对象:

static void tls_dealloc(void *p) 
{
    // # define EMPTY_POOL_PLACEHOLDER ((id*)1)
    // 如果 p 是空占位池则 return
    if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
        // No objects or pool pages to clean up here.
        // 这里没有 objects 或者 pages 需要清理
        return;
    }

    // reinstate TLS value while we work
    // 这里直接把 p 保存在 TLS 中作为 hotPage
    setHotPage((AutoreleasePoolPage *)p);

    if (AutoreleasePoolPage *page = coldPage()) {
        // 如果 coldPage 存在(双向链表中的第一个 page)
        
        // 这个调用的函数链超级长,最终实现的是把自动释放池里的所有自动释放对象都执行
        // objc_release 然后所有的 page 执行 delete 
        if (!page->empty()) objc_autoreleasePoolPop(page->begin());  // pop all of the pools
        
        // OPTION( DebugMissingPools, 
        //         OBJC_DEBUG_MISSING_POOLS,
        //         "warn about autorelease with no pool in place, which may be a leak")
        // 警告没有池的自动释放,这可能是泄漏
        
        // OPTION( DebugPoolAllocation,
        //         OBJC_DEBUG_POOL_ALLOCATION,
        //         "halt when autorelease pools are popped out of order,
        //          and allow heap debuggers to track autorelease pools")
        // 当自动释放池顺序弹出时暂停,并允许堆调试器跟踪自动释放池
        
        if (slowpath(DebugMissingPools || DebugPoolAllocation)) {
            // pop() killed the pages already
        } else {
            // 从 page 开始一直沿着 child 向前把所有的 page 执行 delete
            // kill 只处理 page,不处理 autorelease 对象
            page->kill();  // free all of the pages
        }
    }
    
    // clear TLS value so TLS destruction doesn't loop
    // 清除 TLS 值,以便 TLS 销毁不会循环
    // 把 hotPage 置为 nil
    // static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    // tls_set_direct(key, (void *)page);
    // 把 key 置为 nil
    setHotPage(nil);
}

综上所述,objc_autoreleasePoolPop出栈的流程如下所示:

image.png

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()释放池子。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容