內(nèi)存管理剖析(六)——autorelease原理分析

經(jīng)歷過MRC時代的開發(fā)者,肯定都用過autorelease方法,用于把對象交給AutoreleasePool管理胜榔,在合適的時候胳喷,自動釋放對象。其實所謂的自動釋放對象夭织,就是對所管理的對象調(diào)用release方法吭露。要想知道autorelease方法的原理,首先就需要弄清楚AutoreleasePool是個什么東東尊惰。

下面來看一個段MRC環(huán)境下的代碼奴饮,為什么要在MRC下討論這個問題呢?因為ARC會為我們在合適的地方自動加上autorelease代碼择浊,并且不允許我們手動調(diào)用該方法了戴卜,為了方便研究autorelease原理,我們還是得回到MRC琢岩。

****************** main.m *****************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

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

    NSLog(@"pool--start");
    @autoreleasepool { 
        CLPerson *p = [[[CLPerson alloc] init] autorelease];
    } 
    NSLog(@"pool--end");

    return 0;
}

************** CLPerson.m **************
#import "CLPerson.h"

@implementation CLPerson

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [super dealloc];
}
@end

****************** 打印結(jié)果 *******************
2019-08-27 16:37:15.141523+0800 Interview16-autorelease[11602:772121] pool--start
2019-08-27 16:37:15.141763+0800 Interview16-autorelease[11602:772121] -[CLPerson dealloc]
2019-08-27 16:37:15.141775+0800 Interview16-autorelease[11602:772121] pool--end

概括一下看到的表面現(xiàn)象:CLPerson實例對象p是在@autoreleasepool {}大括號結(jié)束的時候被釋放的投剥。
那么@autoreleasepool {}到底做了什么呢?我們在命令行窗口里對main.m文件執(zhí)行如下命令

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

在生成的中間代碼main.cpp中担孔,找到main函數(shù)的底層實現(xiàn)如下

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
    }
    return 0;
}

其實如果你熟悉消息機制江锨,上述的代碼可以轉(zhuǎn)化成如下形式

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { 
        __AtAutoreleasePool __autoreleasepool; 
        CLPerson *p = [[[CLPerson alloc] init] autorelease];
    }
    return 0;
}

我們觀察可發(fā)現(xiàn)@autoreleasepool {}經(jīng)過編譯之后發(fā)生了如下轉(zhuǎn)變

這里多了個__AtAutoreleasePool,它其實是個c++的結(jié)構(gòu)體糕篇,可以在main.cpp里搜索到它的定義如下

struct __AtAutoreleasePool {
    //構(gòu)造函數(shù)-->可以類比成OC的init方法啄育,在創(chuàng)建時調(diào)用
  __AtAutoreleasePool()
    {
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    
    //析構(gòu)函數(shù)-->可以類比成OC的dealloc方法,在銷毀時調(diào)用
  ~__AtAutoreleasePool()
    {
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    
  void * atautoreleasepoolobj;
};

如果你還不了解C++語法也無妨拌消,它跟OC的類相似挑豌,可以有函數(shù)(方法),上面的這個結(jié)構(gòu)體__AtAutoreleasePool里面有已經(jīng)有兩個函數(shù)墩崩,

  • 一個構(gòu)造函數(shù)__AtAutoreleasePool() --> atautoreleasepoolobj = objc_autoreleasePoolPush();氓英,結(jié)構(gòu)體被創(chuàng)建時調(diào)用,用于結(jié)構(gòu)體的初始化
  • 一個析構(gòu)函數(shù)~__AtAutoreleasePool() --> objc_autoreleasePoolPop(atautoreleasepoolobj);鹦筹,結(jié)構(gòu)體被銷毀時調(diào)用

再回到我們的main函數(shù)铝阐,其實它本質(zhì)上就是下面這個形式

上面是單層@autoreleasepool {}的情況,那么如果有多層@autoreleasepool {}嵌套在一起铐拐,就可以按照同樣的規(guī)則來拆解

objc_autoreleasePoolPush() & objc_autoreleasePoolPop()

接下來我們就來探究一下這兩個函數(shù)的實現(xiàn)邏輯徘键。在objc4源碼的NSObject.mm文件里可以找到它們的實現(xiàn)

*************** NSObject.mm (objc4) ******************
void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

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

可以看到,它們分別調(diào)用了C++類 AutoreleasePoolPagepush()pop()函數(shù)遍蟋。要想繼續(xù)深入后續(xù)函數(shù)的實現(xiàn)邏輯吹害,我們需要先來看一看這個AutoreleasePoolPage的內(nèi)部結(jié)構(gòu),它的內(nèi)容不少匿值,有大量函數(shù)赠制,但是我們首先需要理清楚它的成員變量赂摆,這些是可變化的挟憔,可操控的钟些,所以去掉函數(shù)和一些靜態(tài)常量,可以將AutoreleasePoolPage結(jié)構(gòu)簡化如下

class AutoreleasePoolPage 
{
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}

根據(jù)其命名绊谭,中文釋義成自動釋放池頁政恍,有個頁的概念。我們知道自動釋放池达传,是用來存放對象的篙耗,這個“頁”就說明釋放池的結(jié)構(gòu)體應(yīng)該有頁面篇幅限制(內(nèi)存空間大小)宪赶。具體多大呢宗弯?來看一下AutoreleasePoolPage的兩個函數(shù)

id * begin() {

        return (id *) ((uint8_t *)this+sizeof(*this));
}

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

begin()函數(shù)返回一個指針,指向自身最后一個成員變量之后的內(nèi)存地址(相當于越過了自身所占用的內(nèi)存空間)
end()里面有一個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

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

可以看到蒙保,SIZE實際上是4096。這就是說end()函數(shù)欲主,得到的是一個指針邓厕,指向AutoreleasePoolPage對象地址之后的第4096個字節(jié)的內(nèi)存地址。

AutoreleasePoolPage的begin()和end()

通過以上掌握的信息扁瓢,我們先拋出結(jié)論详恼,然后再繼續(xù)通過源碼加深理解。

每個AutoreleasePoolPage對象占4096個字節(jié)引几,其中成員變量共占用 8字節(jié) * 7 = 56個字節(jié)昧互。剩余的4040個字節(jié)的空間就是用來存儲自動釋放對象的。

因為一個AutoreleasePoolPage對象的內(nèi)存是有限的伟桅,程序里面可能有很多對象會被加入自動釋放池硅堆,因此可能會出現(xiàn)多個AutoreleasePoolPage對象來共同存放自動釋放對象。所有的AutoreleasePoolPage對象是以雙向鏈表的形式(數(shù)據(jù)結(jié)構(gòu))連接在一起的贿讹。

AutoreleasePoolPage對象的各成員變量含義如下

  • magic_t const magic;
  • id *next;指向AutoreleasePoolPage內(nèi)下一個可以用來存放自動釋放對象的內(nèi)存地址
  • pthread_t const thread; 自動釋放池所屬的線程渐逃,說明它不能跟多個線程關(guān)聯(lián)。
  • AutoreleasePoolPage * const parent;指向上一頁釋放池的指針
  • AutoreleasePoolPage *child;指向下一頁釋放池的指針
  • uint32_t const depth;
  • uint32_t hiwat;
    AutoreleasePoolPage結(jié)構(gòu)示意圖

【第一次AutoreleasePoolPage::push();】

接下來民褂,我們就正式開始研究AutoreleasePoolPage::push();茄菊。假設(shè)我們現(xiàn)在是處在項目的main函數(shù)的第一個@autoreleasepool {}開始的地方,也就是整個程序?qū)谝淮稳フ{(diào)用push()函數(shù):

#   define POOL_BOUNDARY nil

static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {//Debug模式下,每個autorelease pool都會創(chuàng)建新頁
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {//標準情況下唤锉,調(diào)用autoreleaseFast()函數(shù)
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

其中POOL_BOUNDARY就是nil的宏定義嚣伐,忽略Debug模式,我們只看正常模式脊僚,那么push()將會調(diào)用autoreleaseFast(POOL_BOUNDARY)得到一個id *dest并將其返回給上層函數(shù)。查看一下這個autoreleaseFast(),看看它到底能給我們返回什么

static inline id *autoreleaseFast(id obj)
    {
        //拿到當前可用的AutoreleasePoolPage對象page
        AutoreleasePoolPage *page = hotPage();
        //(1)如果page存在&&page未滿辽幌,則直接增加obj
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {//(2)如果滿了增淹,則調(diào)用autoreleaseFullPage(obj, page);
            return autoreleaseFullPage(obj, page);
        } else {//(3)如果沒有頁面,則調(diào)用autoreleaseNoPage(obj);
            return autoreleaseNoPage(obj);
        }
    }

因為是整個程序第一次push操作乌企,因此page對象還不存在虑润,所以會按照情況(3)走,也就是autoreleaseNoPage(obj);加酵,實現(xiàn)如下

static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
    {
        
        /*--"No page"
         1.可以表示當前還沒有任何pool被創(chuàng)建(pushed)
         2.也可以表示已經(jīng)創(chuàng)建了一個empty placeholder pool(空釋放池占位符)拳喻,只是還沒添加任何內(nèi)容
         */
        assert(!hotPage());
        
        
        
        
        
        
        //標簽-->是否需要增加額外的POOL_BOUNDARY
        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            /*
             如果存在EmptyPoolPlaceholder(空占位符pool),就修改標簽為true猪腕,
             后面就需要依據(jù)此標簽增加額外的POOL_BOUNDARY
             */
            pushExtraBoundary = true;
        }
        
        /*
         如果傳入的obj不等于POOL_BOUNDARY(nil)并且找不到當前pool(丟失了)冗澈,返回nil
         */
        else if (obj != POOL_BOUNDARY  &&  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", 
                         pthread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        
        /*
         ????????如果傳入的是POOL_BOUNDARY,并且不在Debug模式陋葡,
         會調(diào)用setEmptyPoolPlaceholder()設(shè)置一個EmptyPoolPlaceholder
         */
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            return setEmptyPoolPlaceholder();
        }
        
        
        

        // 初始化第一個AutoreleasePoolPage
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        //將其設(shè)置成當前頁(hot)
        setHotPage(page);
        
        // 根據(jù)pushExtraBoundary標簽決定是否多入棧一個POOL_BOUNDARY
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // 將傳入的obj入棧渗柿,通過 add()函數(shù)
        return page->add(obj);
    }

因為此時還沒有創(chuàng)建過AutoreleasePoolPage,并且也沒有設(shè)置過EmptyPoolPlaceholder脖岛,因此程序會命中代碼中????????標記出的代碼朵栖,調(diào)用setEmptyPoolPlaceholder();,該函數(shù)實現(xiàn)如下

#   define EMPTY_POOL_PLACEHOLDER ((id*)1)
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
********************************************

static inline id* setEmptyPoolPlaceholder()
    {
        assert(tls_get_direct(key) == nil);
        tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
        return EMPTY_POOL_PLACEHOLDER;
    }

可以看到實際上就是將key(id*)1綁定起來柴梆,這個key是一個靜態(tài)常量陨溅,最后將這個(id*)1作為一個空釋放池池占位符返回,這樣整個程序的第一個push()函數(shù)結(jié)束绍在,結(jié)果是生成了一個EMPTY_POOL_PLACEHOLDER (也就是(id*)1)作為釋放池占位符门扇。

【第一次調(diào)用autorelease】

接著上面的過程,我們在push()后偿渡,第一次對某個對象執(zhí)行autorelease方法時臼寄,看一下autorelease的內(nèi)部做了什么,先找到其源碼如下

- (id)autorelease {
    return ((id)self)->rootAutorelease();//???從這里往下走
}

************************************************
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());
        id *dest __unused = autoreleaseFast(obj);//??最終走到了這個方法
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }


通過逐層遞進溜宽,我們看到autorelease方法最終又來到了autoreleaseFast()函數(shù)

static inline id *autoreleaseFast(id obj)
    {
        //拿到當前可用的AutoreleasePoolPage對象page
        AutoreleasePoolPage *page = hotPage();
        //(1)如果page存在&&page未滿吉拳,則直接增加obj
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {//(2)如果滿了,則調(diào)用autoreleaseFullPage(obj, page);
            return autoreleaseFullPage(obj, page);
        } else {//(3)如果沒有頁面适揉,則調(diào)用autoreleaseNoPage(obj);
            return autoreleaseNoPage(obj);
        }
    }

那么這一次留攒,我們看看第一句代碼里面hotPage();得到的是什么

static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        //如果檢查到key有綁定EMPTY_POOL_PLACEHOLDER,返回nil
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        
        if (result) result->fastcheck();
        return result;//將當前頁對象返回
    }

因為我們一開始將keyEMPTY_POOL_PLACEHOLDER綁定過嫉嘀,因此這里返回空炼邀,表明當前頁空,還未被創(chuàng)建剪侮,因此我們返回到autoreleaseFast方法里面拭宁,將會調(diào)用autoreleaseNoPage(obj)函數(shù),根據(jù)我們上面對這個函數(shù)步驟的注釋,這一次程序應(yīng)該會走到函數(shù)的最后一部分

主要做了下面幾件事:

  • 初始化第一個AutoreleasePoolPage
  • 將其設(shè)置成當前頁(hot)
  • 最初的EMPTY_POOL_PLACEHOLDER會使pushExtraBoundary置為true杰标,因此這里需要為第一個AutoreleasePoolPage先入棧一個POOL_BOUNDARY
  • 最后用add(obj)將傳入的自動釋放對象obj入棧

上面add()函數(shù)的具體功能兵怯,其實就是將obj的值賦值給當前AutoreleasePoolPagenext指針指向的內(nèi)存空間,然后next再進行++操作在旱,移向下一段可用內(nèi)存空間,方便下一次存放自動釋放對象的時候使用推掸。如下

id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;//先賦值桶蝎,再++
        protect();
        return ret;
    }

另外需要注意一下這里的setHotPage(page)函數(shù),實現(xiàn)如下

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

它的作用就是把當前新創(chuàng)建的AutoreleasePoolPagekey綁定起來谅畅,日后hotPage()函數(shù)就可以通過key直接拿到當前頁登渣。

【再一次調(diào)用autorelease】

如果我們繼續(xù)對新的對象執(zhí)行autorelease操作,同樣會來到函數(shù)毡泻,但由于AutoreleasePoolPage對象已經(jīng)存在了胜茧,如果當前page未滿,會走如下函數(shù)

image.png

也就是直接通過add(obj)函數(shù)將obj對象入棧

我們之前說過仇味,一個AutoreleasePoolPage對象能存放的自動釋放對象數(shù)量是有限的呻顽,一個自動釋放對象就是一個指針,占8字節(jié)丹墨,而AutoreleasePoolPage對象可用的空間是4040個字節(jié)廊遍,也就是可以存放505個對象(指針),所以一頁AutoreleasePoolPage是有可能滿頁的贩挣,這個時候喉前,autoreleaseFast就會調(diào)用autoreleaseFullPage(obj, page);函數(shù),它的實現(xiàn)如下

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 {//通過child指針拿到下一個沒有滿的page對象
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);//先將上面獲取的page設(shè)置為當前頁(hot)
        return page->add(obj);//通過add函數(shù)將obj存入該page
    }

其實上面就是通過AutoreleasePoolPage對象的child指針去尋找下一個未滿的pageAutoreleasePoolPage對象之間是通過childparent指針形成的雙向鏈表結(jié)構(gòu)王财,就是為了在這個時候使用的卵迂。同樣,在清空釋放池對象的時候绒净,如果當前釋放池完全空了见咒,則會通過parent指針去尋找上層的釋放池。

【再一次AutoreleasePoolPage::push();】

除了系統(tǒng)在main函數(shù)里加上的最初的一層@autoreleasepool {}之外挂疆,有時候我們自己的代碼里面可能會也會使用@autoreleasepool {}论颅,方便對一些對象進行更為靈活的內(nèi)存管理。那么我們手動加的@autoreleasepool {}肯定是嵌套在main函數(shù)@autoreleasepool {}內(nèi)部的囱嫩,相當于

int main(int argc, const char * argv[]) {
        @autoreleasepool {//這是系統(tǒng)加的第一層
                @autoreleasepool {}//這是我們可能會添加的內(nèi)層嵌套
        }

}

現(xiàn)在我們再次來看一下這一次AutoreleasePoolPage::push();會如何執(zhí)行恃疯。同樣程序會執(zhí)行到autoreleaseFast(POOL_BOUNDARY);

POOL_BOUNDARY會被傳入autoreleaseFast函數(shù),并且也會通過add()或者autoreleaseFullPage()被添加到AutoreleasePoolPage對象的頁空間上墨闲。其實就是和普通的[obj autorelease]的流程一樣今妄,只不過這次是obj = POOL_BOUNDARY,顯然這是為了一個新的@autoreleasepool{}做準備。

POOL_BOUNDARY到底是拿來干嘛的呢盾鳞?一會你就知道了犬性。

分析完了源碼,現(xiàn)在通過圖例來展示一下@autoreleasepool的實現(xiàn)原理腾仅。
【假設(shè)】為方便展示每頁AutoreleasePoolPage只能存放3個釋放對象乒裆,如下

autorelease對象什么時候回調(diào)用release方法呢?

這個問題就要搞清楚@autoreleasepool{}的另一半AutoreleasePoolPage::pop(atautoreleasepoolobj);做了什么推励。一起來看一看

其中的核心函數(shù)便是releaseUntile(stop)鹤耍,這里的stop實際上傳入的就是POOL_BOUNDARY,進入該函數(shù)

void releaseUntil(id *stop) 
    {
        
        
        while (this->next != stop) {//??如果next指向POOL_BOUNDARY验辞,跳出循環(huán)??
            
            //??拿到當前頁
            AutoreleasePoolPage *page = hotPage();

            //????當前頁如果為空稿黄,通過parent拿到上一個AutoreleasePoolPage對象作為當前頁
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            
            //??????通過 --next 拿到當前頁棧頂?shù)膶ο?            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                //????????如果obj不是POOL_BOUNDARY,就進行[obj release]
                objc_release(obj);
            }
        }

        setHotPage(this);
    }

pop()核心步驟已經(jīng)在上面函數(shù)里的注釋體現(xiàn)出來跌造。也就是說杆怕,當最內(nèi)層的@autoreleasepool{}作用域結(jié)束調(diào)用其對應(yīng)的pop()函數(shù)時,會從AutoreleasePoolPage鏈表的當前頁里面找到棧頂?shù)膶ο罂翘埃饌€開始釋放陵珍,直到遇到POOL_BOUNDARY就停下來,這樣违施,就代表這一層的@autorelease{}內(nèi)所包含的所有對象都完成了release方法調(diào)用撑教。

當程序走到上一層的@autoreleasepool{}作用域結(jié)束的地方,又回執(zhí)行上面的流程醉拓,對其包含的對象一次調(diào)用release方法伟姐。可以通過下圖的示例來體會一下亿卤。

AutoreleasePoolPage::pop()的核心步驟


AutoreleasePool與RunLoop

通過上面的研究愤兵,我們知道@autoreleasepool{}的作用,實際上就是在作用域的頭和尾分別調(diào)用了objc_autoreleasePoolPush();objc_autoreleasePoolPop()函數(shù)排吴,但是在iOS項目當中秆乳,@autoreleasepool{}的作用域是什么時候開始,什么時候結(jié)束呢钻哩?這就需要了解我們之前研究過的另一個知識點RunLoop屹堰。我們知道,除非我們手動啟動子線程的RunLoop街氢,否則程序里面只有主線程有RunLoop扯键,這是系統(tǒng)默認開啟的。下面我們來看一下主線程的RunLoop肚子里都有什么寶貝珊肃。

我們可以隨便新建一個iOS項目荣刑,在ViewControllerviewDidLoad方法里可以直接打印當前RunLoop對象(即主線程的RunLoop對象)

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%@",[NSRunLoop currentRunLoop]);
}

@end

打印結(jié)果是洋洋灑灑的一大堆馅笙,如果你還不熟悉RunLoop的結(jié)構(gòu),可以參考我的Runloop的內(nèi)部結(jié)構(gòu)與運行原理厉亏,里面應(yīng)該說的比較清楚了董习。我們可以在打印結(jié)果的common mode items部分,找到兩個跟autorelease相關(guān)的observer爱只,如下圖所示

runloop中的autorelease

具體如下

<CFRunLoopObserver 0x600003f3c640 [0x10a2fdae8]>
{
valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, 
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e17ac9d), 
context = 
<CFArray 0x6000000353b0 [0x10a2fdae8]>
    {
    type = mutable-small, count = 1, values = (0 : <0x7f91ff802058>)
    }
}


<CFRunLoopObserver 0x600003f3c500 [0x10a2fdae8]>
{
valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, 
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e17ac9d), 
context = 
<CFArray 0x6000000353b0 [0x10a2fdae8]>
    {
    type = mutable-small, count = 1, values = (0 : <0x7f91ff802058> )
    }
}

我們可以看到皿淋,這兩個監(jiān)聽器分監(jiān)聽的狀態(tài)分別是

  • activities = 0xa0(對應(yīng)十進制的160
  • activities = 0x1(對應(yīng)十進制的1
    這兩個狀態(tài)怎么解讀呢?我們可以在CF框架的RunLoop源碼里面找到對應(yīng)的定義
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),************十進制1---(進入loop)
    kCFRunLoopBeforeTimers = (1UL << 1),****十進制2
    kCFRunLoopBeforeSources = (1UL << 2),**十進制4
    kCFRunLoopBeforeWaiting = (1UL << 5),***十進制32----(loop即將休眠)
    kCFRunLoopAfterWaiting = (1UL << 6),*****十進制64
    kCFRunLoopExit = (1UL << 7),**************十進制128----(退出loop)
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

根據(jù)RunLoop狀態(tài)的枚舉值可以看出恬试,160 = 128 + 32窝趣,也就是說

  • activities = 0xa0 =(kCFRunLoopExitkCFRunLoopBeforeWaiting
  • activities = 0x1 =(kCFRunLoopEntry
    因此這三個狀態(tài)被監(jiān)聽到的時候,就會調(diào)用_wrapRunLoopWithAutoreleasePoolHandler函數(shù)忘渔。這個函數(shù)實際上是按照下圖的示意運作
  • 監(jiān)聽到kCFRunLoopEntry事件高帖,調(diào)用objc_autoreleasePoolPush();
  • 監(jiān)聽到kCFRunLoopBeforeWaiting事件缰儿,調(diào)用objc_autoreleasePoolPop()畦粮,然后調(diào)用objc_autoreleasePoolPush();
  • 監(jiān)聽到kCFRunLoopExit事件,調(diào)用objc_autoreleasePoolPop()

根據(jù)上面的分析乖阵,我們可以總結(jié)宣赔,除了程序啟動(對應(yīng)kCFRunLoopEntry)和程序退出(對應(yīng)kCFRunLoopExit)會調(diào)用一次objc_autoreleasePoolPush();objc_autoreleasePoolPop()外,程序的運行過程中瞪浸,每當RunLoop即將休眠儒将,被observer監(jiān)聽到kCFRunLoopBeforeWaiting狀態(tài)時,會先調(diào)用一次objc_autoreleasePoolPop()对蒲,這樣就將當前的autoreleasepool里面的對象逐個調(diào)用release方法钩蚊,相當于清空釋放池子;緊接著再調(diào)用一次objc_autoreleasePoolPush();蹈矮,相當于開啟一個新的釋放池砰逻,等待RunLoop醒來后的下一次循環(huán)使用。

自動釋放池的對象什么時候會被調(diào)用release方法呢泛鸟?
RunLoop的每一圈循環(huán)過程中蝠咆,調(diào)用過autorelease方法的對象(也就是被加入AutoreleasePoolPage的對象),會在當次循環(huán)即將進入休眠狀態(tài)的時候北滥,被調(diào)用release方法,也可以說是被釋放了刚操。

好了,AutoreleasePool的原理以及它和RunLoop的關(guān)系就分析到這里再芋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末菊霜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子济赎,更是在濱河造成了極大的恐慌占卧,老刑警劉巖遗菠,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異华蜒,居然都是意外死亡辙纬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門叭喜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贺拣,“玉大人,你說我怎么就攤上這事捂蕴∑┪校” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵啥辨,是天一觀的道長涡匀。 經(jīng)常有香客問我,道長溉知,這世上最難降的妖魔是什么陨瘩? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮级乍,結(jié)果婚禮上舌劳,老公的妹妹穿的比我還像新娘。我一直安慰自己玫荣,他們只是感情好甚淡,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著捅厂,像睡著了一般贯卦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上焙贷,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天撵割,我揣著相機與錄音,去河邊找鬼盈厘。 笑死睁枕,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的沸手。 我是一名探鬼主播外遇,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼契吉!你這毒婦竟也來了跳仿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤捐晶,失蹤者是張志新(化名)和其女友劉穎菲语,沒想到半個月后妄辩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡山上,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年眼耀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佩憾。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡哮伟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出妄帘,到底是詐尸還是另有隱情楞黄,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布抡驼,位于F島的核電站鬼廓,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏致盟。R本人自食惡果不足惜碎税,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望勾邦。 院中可真熱鬧蚣录,春花似錦割择、人聲如沸眷篇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蕉饼。三九已至,卻和暖如春玛歌,著一層夾襖步出監(jiān)牢的瞬間昧港,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工支子, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留创肥,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓值朋,卻偏偏與公主長得像叹侄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子昨登,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容