Autoreleasepool

autoreleasepool:自动释放池。在aotureleasepool中创建的对象,在自动释放池销毁时,对所有的对象做release操作。
一般在程序的入口都会有如下代码:

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

在这里 @autoreleasepool{},编译器会在大括号前后插入两行代码:

objc_autoreleasePoolPush();
// 大括号之间的代码
objc_autoreleasePoolPop();

那么此处objc_autoreleasePoolPush();objc_autoreleasePoolPush();都做了哪些事情呢?

源码解读

查看objc4-818.2源码,由于源码中autorelease被放到了NSObject.mm文件中,该文件又比较大,所以以下代码皆是做了简化的代码。
在看具体的autoreleasepool之前,我们先看下苹果官方对于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,它是自动释放池的边界。
自动释放池的token是指向该池的POOL_BOUNDARY的指针。当池被弹出时,优先释放对象,其次是哨兵。
堆栈分为两个双向链接的页面列表。 根据需要添加和删除页面。
TLS变量存储在热页面,该页面存储新自动释放的对象。

注:TLS(线程本地存储)是一种在多线程时使用的技术,它可以使你的全局变量、静态变量以及局部静态、静态成员变量成为线程独立的变量,即每个线程的TLS变量之间互不影响。例如:linux下的全局变量 errno,windows下的GetLastError ,线程A在设置了一个错误信息后,线程B又设置了一个错误信息,前一个线程设置的信息就被覆盖了。解决方法就是将这个全局变量设置为TLS变量,这样在用户看来errno是一个全局变量,实际上它是每个线程独立的。

AutoreleasePoolPage的介绍:

AutoreleasePoolPageData定义

struct AutoreleasePoolPageData
{
    //检查校验完整性的变量。
    magic_t const magic;
    //指向新加入的autorelease对象。
    __unsafe_unretained id *next;
    //当前page所在的线程。
    pthread_t const thread;
    //指向前一个page的指针。
    AutoreleasePoolPage * const parent;
    //指向后一个page的指针。
    AutoreleasePoolPage *child;
    //双向链表的深度,即该链表中结点的个数。
    uint32_t const depth;
    //数据容纳的一个上限。
    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)
    {
    }
};

AutoreleasePoolPage定义


class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    friend struct thread_data_t;

    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil

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
    
private:
    static pthread_key_t const key = 38;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const COUNT = SIZE / sizeof(id);
}

这里AutoreleasePoolPage与AutoreleasePoolPageData有一个继承关系,AutoreleasePoolPageData主要存放结构数据;

  • EMPTY_POOL_PLACEHOLDER占位的空池子):当一个pool恰好被push并且它绝不包含任何对象的时候,EMPTY_POOL_PLACEHOLDER存储在TLS(Thread-local storage 存储本地的线程)中。当顶层(即libdispatch)推送和弹出pools,却没有使用这些pools的时候,可以节省内存开销。
  • POOL_BOUNDARY边界值,是一个nil对象;
  • SIZE AutoreleasePoolPage的大小,一般是4K;
  • COUNT一个page里对象数
根据其结构数据,我们可以看出AutoreleasePool整体结构是一个由AutoreleasePoolPage为节点构成双向列表,其中parentchild分别指向父节点和子节点。next则指向节点内部的需要autorelease的对象。
流程图.png

搜索objc_autoreleasePoolPush我们便可看到整个autoreleasepool的入口:
这里主要通过三个函数来分析Autoreleasepool的核心功能

objc_autoreleasePoolPush

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::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);
        printf("%p\n", dest);

        return dest;
    }

DebugPoolAllocation判断是否是调试模式,这里涉及到两个方法autoreleaseNewPageautoreleaseFast

   id *autoreleaseNewPage(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page) return autoreleaseFullPage(obj, page);
        else return autoreleaseNoPage(obj);
    }

    static inline AutoreleasePoolPage *hotPage()
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }
static inline void *tls_get_direct(tls_key_t k)
{
//    ASSERT(is_valid_direct_key(k));

    if (_pthread_has_direct_tsd()) {
        return _pthread_getspecific_direct(k);
    } else {
        return pthread_getspecific(k);
    }
}

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

当在调试模式时,调用hotPage来获取AutoreleasePoolPage;hotPage中通过tls_get_direct来获取tls线程所维护的hotpage;

1、上文中也提到TLS线程,TLS是通过线程key 去获取该线程所维护的hotPage,
2、如果把hotPage看做是一个全局的变量,那么在多线程操作中,多线程更改参数就会造成数据污染;所以就用到了TLS变量;
3、如果直接获取TLS变量,在有大量使用TLS变量的地方,寻址所消耗的时间将会增加一倍,所以一般使用内联函数
4、对照 setHotPage发现tls_set_direct传入的为key-value,即pthread_key_t线程key值与当前AutoreleasePoolPage;那么在取的时候,取的也就是当前AutoreleasePoolPage

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

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;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

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

id *autoreleaseNoPage(id obj)
    {

  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;
        }
        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        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);
    }

快速获取自动释放池,先通过hotPage获取,也就是tls线程关联的page;
这里有三种情况:

  • 如果hotpage有值,并且还没满,直接addobj
  • 如果hotpage有值,但是当前page满了,调用autoreleaseFullPage,并传入obj与当前hotpage;在autoreleaseFullPage中,通过page->child获取当前hotpage的子page,并判断是否满了:
    -- 如果满了则继续查找child-page,直到child-page不满,设置child-page为hotpage,添加obj;
    -- 或者child-page为空则重新生成一个新的page,new-page设置为hotPage并添加obj
  • 如果hotpage没值,则调用autoreleaseNoPage,重新生成一个新的page,并设置为hotPage,然后添加obj
    这里要注意的是pushExtraBoundary设置边界值也就是POOL_BOUNDARY当时空page的时候设置nil边界。具体可看流程图
 id *add(id obj)
    {
        printf("%p----\n", this);
        ASSERT(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

objc_autorelease

id
objc_autorelease(id obj)
{
    return AutoreleasePoolPage::autorelease(obj);

}

static inline id autorelease(id obj)
{
        ASSERT(obj);
//        ASSERT(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        printf("%p\n", dest);

        return obj;
 }

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

这里objc_autorelease调用autorelease,然后调用autoreleaseFast,处理过程如第一部分的autoreleaseFast一致

objc_autoreleasePoolPop

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}
static inline void
    pop(void *token)
    {
        //token为自动释放池中存储的要被释放的对象,当创建出autoreleasePool后如果没有存放数据token则为EMPTY_POOL_PLACEHOLDER的地址
        AutoreleasePoolPage *page;
        id *stop;
        
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            //如果token存在,但是自动释放池中指向的是一个空池,(有可能自动释放池创建了出来,但是什么对象都没有存)
            // Popping the top-level placeholder pool.
            //则先取到当前hotpage
            page = hotPage();
            if (!page) {
                //如果hotpage为空,则把当前线程所关联的page置为nil,然后返回(也就相当于pop了所有对象)
                // Pool was never used. Clear the placeholder.
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            //在hotpage有值的情况(pool被使用)下,查找hotpage的父节点(由于其父节点有可能为nil,所以递归查找其直属父节点),
            page = coldPage();
            //这里将指针指向该page的开始
            token = page->begin();
        } else {
            //如果token不指向EMPTY_POOL_PLACEHOLDER,返回该page的首地址
            page = pageForPointer(token);
        }

        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            //这里应该是增强一次判断,判断当前page是否指向开始,以及其父parent节点为空,上边的方法中做了这些处理,相当于清空当前page数据,否则执行badPop
            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 (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }
  //最终执行popPage
        return popPage<false>(token, page, stop);
    }

pop方法中主要的操作写到了注释中,这里需要注意的几个点:

pageForPointer 其实是根据token来获取token所在page的起始地址的方法
    static AutoreleasePoolPage *pageForPointer(const void *p)
    {
        return pageForPointer((uintptr_t)p);
    }

    static AutoreleasePoolPage *pageForPointer(uintptr_t p)
    { 
        AutoreleasePoolPage *result;
        
        //该地址取余page的大小,得出该地址的偏移量相对于该page的偏移量
        uintptr_t offset = p % SIZE;

        ASSERT(offset >= sizeof(AutoreleasePoolPage));
        //获取该page的起始地址;这里类似于比如说page每页size大小为10,然后传入的地址p为233,则该page的首地址为 233%10=3,然后233-3=230,也就是说该page的起始地址为230;
        
        //因为page是以4K大小分页的,所以可以用上述方法
        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();

        return result;
    }

popPage循环删除page上存储的autorelease对象
static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();
        //循环遍历删除page上存储的数据,并把autorelease对象置为SCRIBBLE
        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
            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();
            }
        }
    }
 void releaseUntil(id *stop)
    {
        // Not recursive: we don't want to blow out the stack
        // if a thread accumulates a stupendous amount of garbage
        
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release
            // autoreleased more objects
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

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

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        setHotPage(this);
    }

这里page->releaseUntil(stop)主要调用memset((void*)page->next, SCRIBBLE, sizeof(*page->next));方法来对page上存储的autorelease对象进行重置
重置之后,当前page调用kill方法,(kill方法官方解释为(free all of the pages)) 然后找到page->parent,然后将page-parent设置为hotpage

其他

kill方法
    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;
        //先用最终子孩子的父节点的child进行置nil,并进行delete关键字删除
        //然后再继续往上找节点,先置nil child,然后删除该page
        //直到删到传入的page为止
        do {
            deathptr = page;
            page = page->parent;
            if (page) {
                page->unprotect();
                page->child = nil;
                page->protect();
            }
            //
            delete deathptr;
        } while (deathptr != this);
    }
其他知识

1.子线程在使用autorelease对象时,如果没有autoreleasepool会在autoreleaseNoPage中懒加载一个出来。
2.在runloop的run:beforeDate,以及一些source的callback中,有autoreleasepool的push和pop操作,总结就是系统在很多地方都差不多autorelease的管理操作。
3.就算插入没有pop也没关系,在线程exit的时候会释放资源,执行AutoreleasePoolPage::tls_dealloc,在这里面会清空autoreleasepool。

    static void tls_dealloc(void *p)
    {
        if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
            // No objects or pool pages to clean up here.
            return;
        }

        // reinstate TLS value while we work
        setHotPage((AutoreleasePoolPage *)p);

        if (AutoreleasePoolPage *page = coldPage()) {
            if (!page->empty()) objc_autoreleasePoolPop(page->begin());  // pop all of the pools
            if (slowpath(DebugMissingPools || DebugPoolAllocation)) {
                // pop() killed the pages already
            } else {
                page->kill();  // free all of the pages
            }
        }
        
        // clear TLS value so TLS destruction doesn't loop
        setHotPage(nil);
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容