iOS原理 AutoreleasePool源码分析

iOS原理 文章汇总

前面介绍了AutoreleasePool的基本概念内存结构,本文将通过objc源码来分析AutoreleasePool的底层实现。

AutoreleasePool的Clang编译

使用@autoreleasePool代码块可以创建一个自动释放池,通过Clang编译后发现底层实现如下:

{
    //创建一个AutoreleasePool对象
    __AtAutoreleasePool *atautoreleasepoolobj = objc_autoreleasePoolPush(); 
    
    //这里创建自动释放的对象,创建的对象会被加入到AutoreleasePool对象里
    ... ...    

   //给所有自动释放的对象发送一次release消息,并销毁AutoreleasePool对象
   objc_autoreleasePoolPop(atautoreleasepoolobj)
}

因此,单个Autoreleasepool的运行过程可以简单地理解为 objc_autoreleasePoolPush[对象 autorelease]objc_autoreleasePoolPop 这三个过程。

push 操作

objc_autoreleasePoolPush函数在底层是调用AutoreleasePoolPagepush函数。在源码中断点调试可知,push函数的调用链为:push -> autoreleaseFast(POOL_BOUNDARY) -> autoreleaseNoPage(POOL_BOUNDARY)

  • push()
static inline void *push() 
{
    id *dest;
    //这个判断不知道哪个实际场景可以触发,运行后都是false。源码注释:halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools
    //老版本源码里没有这个判断,直接走 dest = autoreleaseFast(POOL_BOUNDARY)
    if (slowpath(DebugPoolAllocation)) {
        //这里基本不会进,而且全局搜索发现autoreleaseNewPage函数只在这里有调用
        // 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);

    //返回的dest就是后面执行pop函数的入参
    return dest;
}

push里直接调用autoreleaseFast(POOL_BOUNDARY)函数,返回的dest即为后面执行pop函数的入参,本质是哨兵地址。

  • autoreleaseFast(POOL_BOUNDARY)
static inline id *autoreleaseFast(id obj)
{
    //获取当前操作的page,hot page
    AutoreleasePoolPage *page = hotPage();
    //判断当前页
    if (page && !page->full()) {
        //如果page存在并且没有满
        return page->add(obj);
    } else if (page) {
        //如果page存在并且已经满了
        return autoreleaseFullPage(obj, page);
    } else {
        //如果page不存在
        return autoreleaseNoPage(obj);
    }
}

这个方法里先获取当前操作的page,然后进行判断:

  • 如果当前页存在,并且没有满,就调用add函数压栈对象。
  • 如果当前也存在且已经满了,就调用autoreleaseFullPage函数创建新页面,并压栈对象。
  • 如果当前页不存在,即自动释放池为nil或者为empty(空的),就调用autoreleaseNoPage函数来设置空白占位符,或者创建第一页。

这里push操作调用的autoreleaseFast,会执行autoreleaseNoPage函数,入参为POOL_BOUNDARY

  • autoreleaseNoPage(POOL_BOUNDARY)
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    //"No page"表示还没创建自动释放池,或者创建了一个空内容的释放池。
    // "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()) {
        // 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;
    }
    //若当前obj不是哨兵,且释放池为nil则报错
    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", 
                     objc_thread_self(), (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    //若当前obj为哨兵,就设置空白占位符并返回。只有push操作时当前obj才是哨兵
    else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        //push操作会走到这里,然后返回
        // 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();
    }

    //从这里开始,下面就是第一次autorelease操作处理的事项
    
    // We are pushing an object or a non-placeholder'd pool.

    // Install the first page.
    //压栈第一个第一个对象时,需要创建第一页
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    //将第一页设置为hot page
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool.
    //压栈第一个对象时,上面判断已经设置了空白占位符,所以需要先压栈哨兵
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    
    // Push the requested object or pool.
    //压栈对象并返回
    return page->add(obj);
}

autoreleaseNoPage函数在push操作和第一次autorelease操作时会调用。在push操作时,会在这里调用setEmptyPoolPlaceholder函数来设置一个空白占位符,表示已创建了一个自动释放池。

//设置空白占位符
static inline id* setEmptyPoolPlaceholder()
{
    ASSERT(tls_get_direct(key) == nil);
    //通过"key-value"形式将空白占位符存储在tls中,并返回空白占位符的地址
    //tls为当前线程的本地存储,用于保存当前线程的一些信息
    tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
    return EMPTY_POOL_PLACEHOLDER;
}

//判断是否设置空白占位符
static inline bool haveEmptyPoolPlaceholder()
{
     //通过key-value获取值来判断
     id *tls = (id *)tls_get_direct(key);
     return (tls == EMPTY_POOL_PLACEHOLDER);
 }

//自动占位符,地址为 0x1
# define EMPTY_POOL_PLACEHOLDER ((id*)1)

//key值
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
# define AUTORELEASE_POOL_KEY  ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)

因此,push操作本质上是设置一个空白占位符,地址为0x1,并存储在当前线程的tls中,通过这个空白占位符来判断自动释放池是否已创建。在push过程中,既没有创建page,也没有压栈哨兵,可以通过下面的代码来印证:

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

        //第一次打印
        _objc_autoreleasePoolPrint();
        
        //第二次打印
        NSObject *obj = [[NSObject alloc] autorelease];
        _objc_autoreleasePoolPrint();
    }

    return 0;
}

//打印结果
//第一次打印
objc[19922]: ##############
objc[19922]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[19922]: 0 releases pending.   //0
objc[19922]: [0x1]  ................  PAGE (placeholder)
objc[19922]: [0x1]  ################  POOL (placeholder)
objc[19922]: ##############
//第二次打印
objc[19922]: ##############
objc[19922]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[19922]: 2 releases pending.   //2:一个哨兵 + 一个对象 
objc[19922]: [0x10080b000]  ................  PAGE  (hot) (cold)
objc[19922]: [0x10080b038]  ################  POOL 0x10080b038
objc[19922]: [0x10080b040]       0x100667360  NSObject
objc[19922]: ##############

可以看到,第一次打印时,只执行了push操作,只有空白占位符,地址为0x1。第二次打印时,已经压栈了第一个对象,此时才创建了第一页page,并压栈了哨兵和对象。

autorelease 操作

给对象发送autorelease消息即可加入到自动释放池,autorelease方法在底层是调用AutoreleasePoolPageautorelease函数,入参为对象本身。

inline id 
objc_object::rootAutorelease()
{
    //若是TaggedPointer对象,直接返回,不处理
    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);
}

//autorelease操作
static inline id autorelease(id obj)
{
    ASSERT(obj);
    ASSERT(!obj->isTaggedPointer());
    //压栈对象
    id *dest __unused = autoreleaseFast(obj);
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

从代码中可知,若对象是TaggedPointer类型,则不能被添加到自动释放池。autorelease操作实际上是通过调用autoreleaseFast函数来压栈对象,实现逻辑为(源码上面已附):

  • hot page不存在,即压栈的是第一个对象,此时是空白的自动释放池,就调用autoreleaseNoPage函数来创建第一页page,并设为hot page,先压栈哨兵,然后压栈对象。
  • hot page存在且没满时,就调用add函数来压栈对象。
  • hot page存在且已满,就调用autoreleaseFullPage函数来新建一页page,再压栈对象。

接下来分析这其中几个关键函数的实现:

  • hotPage
//设置hot page
static inline void setHotPage(AutoreleasePoolPage *page) 
{
    if (page) page->fastcheck();
    //通过key-value形式,将当前page存储在tls中,即为hot page
    tls_set_direct(key, (void *)page);
}

//获取hot page
static inline AutoreleasePoolPage *hotPage() 
{
     //从当前线程的tls中通过key获取到page,即为hot page
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
}

hot page和上面的空白占位符一样,存储在当前线程的tls中,通过key来读写。

  • add
id *add(id obj)
{
    ASSERT(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    //先从next指针指向的地址里开始存放obj的地址,为8字节
    //再将next指针像高地址偏移8字节,指向的地址用于存放下一次添加的obj的地址
    *next++ = obj;
    protect();
    return ret;
}

add(obj)实际上是先从当前next指针指向的地址里开始存放obj的地址,为8字节大小,再将next指针像高地址偏移8字节,新指向的地址用于存放下一次添加的obj的地址。当压栈第一个对象时,需要先压栈哨兵。

//在第一页里先压栈哨兵
page->add(POOL_BOUNDARY);

# define POOL_BOUNDARY nil

POOL_BOUNDARYnil,说明压栈哨兵其实只是将next指针偏移8字节,从新地址上开始存放对象地址。因此,哨兵其实是自动释放池中对象的边界,而这个边界地址就是在后面执行pop操作时的入参。

  • autoreleaseFullPage

压栈对象时,若hot page满了,就调用autoreleaseFullPage函数来新建page,再压栈。

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);
     
    //这个函数传进来的已经是hot page
    //do-while循环获取没满的page
    do {
        //如果存在子页面,就用子页面替换当前page来判断
        if (page->child) page = page->child;
        //如果没有子页面,而当前page已满,则新建页面
        else page = new AutoreleasePoolPage(page);
    } while (page->full());   //判断page是否已满

    //将page设置为hot page
    setHotPage(page);
    //压栈对象
    return page->add(obj);
}

//判断page是否已满
bool full() { 
   //通过next的值和page的结束地址来判断
    return next == end();
}

//page的end地址
id * end() {
    //this为当前page的首地址,通过首地址加上4096字节,就可得出当前page的结束地址
    return (id *) ((uint8_t *)this+SIZE);
}

//SIZE即为每页AutoreleasePoolPage的大小,为4096字节
static size_t const SIZE = 4096

因此,压栈对象时,若hot page已满,通过do-while循环来判断每一个子页面是否已满,若没满,则将当前子页面设为hot page,若都满了,就新建一个页面并设为hot page,再压栈对象。判断page是否已满,是通过将page的结束地址和next指针对比,若相等,则表示page已满。

pop 操作

pop操作是向自动释放池里的每个对象发送一次release消息,然后销毁掉释放池里的页面。objc_autoreleasePoolPop函数在底层是调用AutoreleasePoolPagepop函数,入参为哨兵地址。

static inline void
pop(void *token)
{
    //传进来的token即为哨兵地址
    
    AutoreleasePoolPage *page;
    id *stop;
    //如果token为空白占位符
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        // Popping the top-level placeholder pool.
        //获取hot page
        page = hotPage();
        if (!page) {
            // Pool was never used. Clear the placeholder.
            //如果page不存在,说明这个自动释放池没有压栈任何对象,直接清除占位符
            //hotPage和空白占位符在tls中存储都是同一个key,所以可以清空
            return setHotPage(nil);
        }

        // Pool was used. Pop its contents normally.
        // Pool pages remain allocated for re-use as usual.
        //容错处理,hot page存在,而哨兵地址异常,则需要更正
        //如果页面存在,获取到第一页
        page = coldPage();
        //将token的值设置为第一页的哨兵地址
        token = page->begin();
    } else {
        //获取token所在的页
        page = pageForPointer(token);
    }

    //将token赋值给stop,则stop此时就是哨兵地址
    stop = (id *)token;
    //容错判断,若stop指向的不是哨兵地址
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
            //若stop指向当前页的begin(),且当前页没有父页面,说明当前也是第一页,stop指向的就是哨兵地址,不做任何处理
            // 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 {
            //若stop指向当前页的begin(),而当前页有父页面,说明stop指向的不是哨兵地址,则报异常并返回
            // Error. For bincompat purposes this is not 
            // fatal in executables built with old SDKs.
            return badPop(token);
        }
    }

    //这个判断基本为false
    if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
        return popPageDebug(token, page, stop);
    }
 
    //出栈页
    return popPage<false>(token, page, stop);
}

pop操作中传进来的token即为哨兵地址,这里主要处理两件事:

  • 如果当前是只有空白占位符的自动释放池,则清除掉空白符,然后返回,这个释放池也就被销毁了。
  • 如果不是空白的自动释放池,先确保token为哨兵地址,然后获取到token所在的页page,并设置结束位stop(也是哨兵地址),最后调用popPage出栈页,完成pop操作。

也就是说,如果当前是空白的自动释放池,pop操作就会清除掉空白占位符,销毁释放池;如果不是空白的自动释放池,pop操作是通过调用popPage函数来出栈页,这个函数的三个入参:tokenstop均为哨兵地址,page为当前哨兵所在的页。如果自动释放池没有嵌套,page就为自动释放池的第一页,如果有嵌套,每个子pool都会有一个哨兵,page就为当前pool哨兵所在的最外层pool中的页。可以在源码中打印这三个参数来印证。

//创建一个pool
@autoreleasepool {
    
    //压栈一个对象
    NSObject *obj = [[NSObject alloc] autorelease];
    //打印pool的内存
    _objc_autoreleasePoolPrint();
}

//在源码的pop(void *token)函数中,最后打印一下popPage函数的三个入参
pop(void *token)
{
    ... ...
    printf(" ==== popPage函数的入参:token = %p, page = %p, stop = %p\n", token, page, stop);
    return popPage<false>(token, page, stop);
}

//打印结果
objc[64136]: ##############
objc[64136]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[64136]: 2 releases pending.
objc[64136]: [0x10180a000]  ................  PAGE  (hot) (cold)
objc[64136]: [0x10180a038]  ################  POOL 0x10180a038
objc[64136]: [0x10180a040]       0x101148a20  NSObject
objc[64136]: ##############
 ==== popPage函数的入参:token = 0x10180a038, page = 0x10180a000, stop = 0x10180a038

从打印结果可以印证,popPage函数的三个入参:tokenstop均为哨兵地址,page为自动释放池的第一页。这个示例中,pool没有嵌套,所以只有一个哨兵,并且处于第一页,也可以试试多嵌套几个子pool,验证下入参情况是否和上面的结论一致。

接下来再分析pop函数中几个关键函数的实现:

  • coldPage
static inline AutoreleasePoolPage *coldPage() 
{
    //获取到当前页
    AutoreleasePoolPage *result = hotPage();
    //while循环获取到自动释放池的第一页
    if (result) {
        while (result->parent) {
            result = result->parent;
            result->fastcheck();
        }
    }
    return result;
}

这个函数其实就是通过hotPage来获取到自动释放池的第一页。

  • begin
id * begin() {
    //当前页的首地址 + 自身成员占用的内存(56字节),即为begin的地址
    return (id *) ((uint8_t *)this+sizeof(*this));
}

页面的首地址加上自身成员占用的56字节大小,就是begin位置。第一页,begin位置为哨兵的起始地址,其它页则为该页第一个对象的起始地址。

  • pageForPointer
static AutoreleasePoolPage *pageForPointer(const void *p) 
{
    //p为传进来的token,即哨兵地址
    return pageForPointer((uintptr_t)p);
}

static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    //SIZE是一页page的大小,为4096字节
    //p % SIZE,是因为当pool嵌套时,需要先release子pool里的对象,通过这个方法可以获取到子pool距其在父pool中所在页起始位置的偏移量
    uintptr_t offset = p % SIZE;

    ASSERT(offset >= sizeof(AutoreleasePoolPage));

   //p - offset,即可获取到子pool所在的父pool的页的起始地址
    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();

    return result;
}

这个函数就是获取哨兵所在的页,当pool嵌套时,先releasepool,就需要获取到子pool在父pool里所处的页。

  • popPage
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    /**
     *  出栈页,这个函数的三个入参:
     *  token:哨兵地址
     *  page:哨兵所在的页,一般为自动释放池的第一页
     *  stop:哨兵地址
     */  
    
    //pop操作入参的allowDebug为false
    if (allowDebug && PrintPoolHiwat) printHiwat();

    //release all objs,给自动释放池中哨兵后的所有对象发送一次release消息
    //执行完这一步,自动释放池里就只剩下空页面了
    page->releaseUntil(stop);

    // memory: delete empty children
    //kill page,移除页面,至少会保留第一页
    if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        //debug判断,pop操作不会进这里,因为传的allowDebug为false
        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
        //debug判断,pop操作不会进这里,因为传的allowDebug为false
        page->kill();
        setHotPage(nil);
    } else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        //有多个页面的自动释放池,在pop操作时会进入这里
        if (page->lessThanHalfFull()) {
            //若当前页的对象数量比一半还少,从第一页的子页面开始kill,只保留第一页
            //如果没有嵌套pool,实际上在上面执行完releaseUntil(stop)后,page都是空页面,所以pop操作基本会走这里
            //如果有嵌套pool,传进来的page可能为子pool处于最外层pool的页,此时release完子pool的对象指针后,当前页保存的外层pool的对象指针数可能会大于页数量的一半,就会触发moreThanHalfFull的条件
            page->child->kill();
        }
        else if (page->child->child) {
            //当前页的对象指针数量比一半还多,且页面数量大于3,就从第三个页开始kill,保留第一页和第二页
            page->child->child->kill();
        }
    }
}

从代码分析可知,pop操作调用popPage来出栈页时,主要做了两个处理:

  • release all objs:给自动释放池中哨兵后的所有对象发送一次release消息。这一步执行完,自动释放池里就只剩下空页面了。
  • kill page:移除页面,只保留第一页,其他页面都会被移除。

接下来再分析popPage函数中几个关键函数的实现:

  • 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
    
    //popPage函数传进来的stop为哨兵地址,this为自动释放池的第一页
    
    //this ->next,即第一页的next指针
    //判断第一页的对象是否被release完
    while (this->next != stop) {
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
       
        //获取到hot page,当前操作的页面,即最后一页
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it
        //如果当前page为空,则获取到父页面,并将父页面设为hot page
        //确保下一次获取的hot page为这个父页面,实现了页面从后往前的遍历
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        //每次都获取当前页的最后一个对象,实现了对象从后往前遍历释放
        id obj = *--page->next;
        //获取完后偏移next指针,确保一直指向当前页的最后一个对象地址
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

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

    //释放完所有的对象后,就将第一页设为hot page
    setHotPage(this);

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

从代码分析可知,releaseUntil函数的处理逻辑为:从后往前遍历自动释放池里的页面,在每一页中再从后往前遍历释放对象,直到哨兵为止。释放完对象后,释放池中就只剩下空的页面,并将第一页设为hot page

  • lessThanHalfFull
//判断当前页是否还没存满一半
bool lessThanHalfFull() {
    //next:当前页最后一个对像指针在页里的地址
    //begin():前面分析了,为当前页的首地址 + 58字节(自身成员的大小)
    //end():当前页的结束地址 
    return (next - begin() < (end() - begin()) / 2);
}

//当前页的结束地址
id * end() {
   //当前页的首地址 + 4096字节,就为当前页的结束地址 
   //每页大小为4096字节
    return (id *) ((uint8_t *)this+SIZE);
}

从代码分析可知,是通过当前页最后一个对象指针在页中地址的偏移量计算,来判断当前页是否存满一半。

  • kill
void kill() 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    
   //pop操作是从自动释放池的第二页开始kill
   //因此这里的this是第二页,可以通过打印输出来印证

    //从第二页开始,循环遍历子页面,最终获取到自动释放池的最后一页
    AutoreleasePoolPage *page = this;
    while (page->child) page = page->child;

    //从最后一页开始往前遍历销毁,直到this(即第二页)销毁后停止
    //每页的处理逻辑为:先获取到当前页面的父页面,再将父页面的子页面设为nil,即销毁了当前页面
    AutoreleasePoolPage *deathptr;
    do {
        deathptr = page;
        //获取到父页面
        page = page->parent;
        if (page) {
            page->unprotect();
            //销毁子页面
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    } while (deathptr != this);
}

从代码分析可知,kill函数是从自重释放池的队后一页开始往前遍历销毁,直到销毁第二页后停止。每一次销毁页面,都是先获取到当前页面的父页面,再将父页面的子页面设为nil,即实现了页面销毁。

因此,pop操作时,如果不是空白的自动释放池,是先从后往前遍历自动释放池里的所有页面,将每一页里的对象从后往前逐步release,然后再次从后往前遍历销毁这些空页面,第一页保留不销毁,最后自动释放池里就只剩下一页空的page,并且为hot page,这就完成了pop操作。

  • 纠正关于pop操作的误解

注意,之前我一直以为pop操作最后会销毁自动释放池,但分析完源码后,才发现只有空白的自动释放池,执行pop操作才会被销毁,而如果存有压栈对象的自动释放池,pop操作只是release对象和kill页面,并没有销毁自动释放池,这点可以通过在源码工程中打印输出来印证。

//在main函数里创建自动释放池
int main(int argc, const char * argv[]) {
     
    @autoreleasepool {

        //往自动释放池里添加`505 * 2`个对象,这样就会创建3页page
        NSInteger count = 505 * 2;
        for(NSInteger i=0; i<count; i++){

            NSObject *obj = [[NSObject alloc] autorelease];
        }

        _objc_autoreleasePoolPrint();
    }
}

//在popPage函数里打印自动释放池的内存
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    ... ...

    //release all objs
    page->releaseUntil(stop);
    //在release结束后打印自动释放池的内存
    _objc_autoreleasePoolPrint();

    //kill page
    ... ....
    //在kill page后再打印自动释放池的内存
    _objc_autoreleasePoolPrint();
}

//在kill函数里打印this,印证是否只kill到第二页
void kill()
{
    // Not recursive: we don't want to blow out the stack
    // if a thread accumulates a stupendous amount of garbage
    printf(" ==== kill() -> this : %p\n", this);
    ... ...
}

//打印结果
//这是在release结束后输出的自动释放池,可以看到里面只剩下三个空page
objc[76175]: ##############
objc[76175]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[76175]: 0 releases pending.
objc[76175]: [0x102809000]  ................  PAGE  (hot) (cold)
objc[76175]: [0x103009000]  ................  PAGE   
objc[76175]: [0x10300b000]  ................  PAGE   
objc[76175]: ##############
//这是在kill函数里打印的this指针,可以看到this为第二页
 ==== kill() -> this : 0x103009000
//这是在kill page后输出的自动释放池,可以看到最后只剩下第一页
objc[76175]: ##############
objc[76175]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[76175]: 0 releases pending.
objc[76175]: [0x102809000]  ................  PAGE  (hot) (cold)
objc[76175]: ##############
Program ended with exit code: 0

从打印结果可以印证,当自动释放池里存有压栈对象时,pop操作只是release对象和kill页面,并没有销毁自动释放池,而且池中只剩下第一页。这点会在后面分析自动释放池嵌套的时候再作说明。

总结

经过对源码的深入分析,我们可以得出以下结论:

  • 一个自动释放池的运行过程分为这三步:push 操作[对象 autorelease]以及 pop 操作

  • push 操作本质上是设置一个空白占位符,地址为0x1,并存储在当前线程的本地存储tls中,设置了这个空白占位符就表示已经创建了一个空的自动释放池。在push过程中,既没有创建page,也没有压栈哨兵。

  • autorelease 操作就是压栈对象,给对象发送autorelease消息,将对象添加到自动释放池中,在这个过程中会做如下处理:

    • 如果压栈的是第一个对象,此时是空白的自动释放池,就创建第一页page,并设为hot page,然后先压栈哨兵,再压栈对象。
    • hot page存在且没满时,直接压栈对象。
    • hot page存在且已满,新建一页page,再压栈对象。
  • pop 操作出栈时,会根据自动释放池的情况分别做不同的处理:

    • 若是空白的自动释放池,即内部只有空白占位符,此时pop操作会清除占位符,销毁自动释放池。
    • 若自动释放池存有压栈对象,就先release对象,再kill页面。最后,自动释放池并没有被销毁,里面还存有一张空的page,也是之前的第一页。
  • 对于不是空白的自动释放池,pop 操作过程中具体做了如下处理:

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

推荐阅读更多精彩内容