iOS關(guān)于自動釋放池

本文是經(jīng)過翻閱博客、論壇學(xué)習(xí)以及代碼調(diào)試的總結(jié),如有疑惑或不準確的地方,歡迎評論溝通指正。

通讀本文你將理解:

  1. 自動釋放池底層結(jié)構(gòu)
  2. 自動釋放池何時釋放(換言之a(chǎn)utorelease何時執(zhí)行release操作)
  3. ARC下什么樣的初始化方法系統(tǒng)會為我們做一次autorelease操作

Objc源碼下載地址https://opensource.apple.com/tarballs/objc4/

一、自動釋放池底層結(jié)構(gòu)

總結(jié)一句話就是:以棧為節(jié)點,以雙向鏈表形式組合而成的一個數(shù)據(jù)結(jié)構(gòu)。通俗一些講自動釋放池是以多個AutoreleasePoolPage為結(jié)點,通過鏈表的方式串連起來的結(jié)構(gòu),這一整串就是自動釋放池。


雙向鏈表結(jié)構(gòu)

結(jié)點對象
  1. magic 用于對當前 AutoreleasePoolPage 完整性的校驗
  2. thread 保存了當前頁所在的線程
  3. id *next指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置
  4. 每個AutoreleasePoolPage大小均為4096


    釋放池結(jié)構(gòu)

二、自動釋放池何時釋放

//ARC 環(huán)境
- (void)viewDidLoad {
    [super viewDidLoad];
   @autoreleasepool {
        YMObject *object = [[YMObject alloc] init];
    };
}

實際的函數(shù)調(diào)用是這樣的

匯編調(diào)用

Pasted Graphic 2.jpg

需要注意的是,整個程序中push和pop的操作都是一一對應(yīng)的,下面會說明
來分析一下 objc_autoreleasePoolPush和objc_autoreleasePoolPop這兩個函數(shù)做了什么

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

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


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

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

Objc4源碼中可以看到實際上都是AutoreleasePoolPage類的push和pop函數(shù)。Push函數(shù)中會調(diào)用一個比較核心的方法: autoreleaseFast

    //這個函數(shù)的作用就是,找到最頂層的一個AutoreleasePoolPage對象,如果沒有那就創(chuàng)建一個;如果找到了,判斷他是否已經(jīng)裝滿了full(),因為一個AutoreleasePoolPage只有4096個字節(jié)大小,如果滿了那就會調(diào)用autoreleaseNoPage()創(chuàng)建一個AutoreleasePoolPage對象并添加add;如果沒滿則直接執(zhí)行add(obj)。
    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();  //hotPage()函數(shù)會對應(yīng)線程去取自動釋放池,這里也可以看出釋放池和線程是一一對應(yīng)的關(guān)系
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }
需要注意的是上述add(obj)中的obj是一個POOL_BOUNDARY對象(可以稱為哨兵對象),并不是我們的autorelease的對象,每次執(zhí)行push操作時都會插入一個哨兵對象,并且把哨兵對象的地址作為返回值返回了,pop函數(shù)需要用到這個哨兵對象的地址,對應(yīng)的每次pop都是尋找到上一個哨兵對象,對期間所有的autorelease對象執(zhí)行一次release操作。

再來看下pop函數(shù):其中會調(diào)用一個比較核心的函數(shù)releaseUntil(id *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
        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中的autorelease對象已釋放完畢則會重新遍歷父結(jié)點的page,知道找到傳遞來的哨兵對象為止
                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);
#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            assert(page->empty());
        }
#endif
    }

上述就是我們自己通過@autoreleasePool{ }形式創(chuàng)建的釋放池的創(chuàng)建和銷毀,以及其內(nèi)部的主要邏輯分析。
當然系統(tǒng)默認也是會在當前runloop結(jié)束本次循環(huán)即將進入休眠的時候也會執(zhí)行一次pop操作來對釋放池內(nèi)的對象進行一次release操作,然后再重新執(zhí)行一次push操作。

再補充一下:(直接從別處搬來的…)

App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()。

  1. 第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池。其 order 是-2147483647,優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
  2. 第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。
總結(jié)可以下來就是:app啟動 調(diào)用了一次push來創(chuàng)建釋放池,中間我們自己可能會通過@autoreleasePool{ }再執(zhí)行n次push和pop操作,runloop即將休眠時又會調(diào)用一次pop和一次push,runloop在Exit時,又會調(diào)用一次pop操作,可以說是一個push就有一個pop與之對應(yīng)的,從而保證對象能正常釋放。

三、ARC下什么樣的初始化方法系統(tǒng)會為我們做一次autorelease操作

ARC下以alloc/new/copy/mutableCopy開頭的方法,其返回的對象歸調(diào)用者所有。歸調(diào)用者所有的意思是:調(diào)用上述四個方法的那段代碼負責釋放方法返回的對象。(摘自Effective Objective-C 2.0 P124)

非alloc/new/copy/mutableCopy開頭的方法編譯器都會自動幫我們調(diào)用autorelease方法。

//自定義類方法
+ (instancetype)createSark {
    return [self new]; //這里是有autorelease的(誰創(chuàng)建誰釋放)
}
// caller
Sark *sark = [Sark createSark]; //此處有retain操作,作用域結(jié)束會有release操作

值得一提的是,ARC下,runtime有一套對autorelease返回值的優(yōu)化策略:

秉著誰創(chuàng)建誰釋放的原則,返回值需要是一個autorelease對象才能配合調(diào)用方正確管理內(nèi)存,于是乎編譯器改寫成了形如下面的代碼:

+ (instancetype)createSark {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 代替我們調(diào)用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我們調(diào)用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相當于代替我們調(diào)用了release

一切看上去都很好,不過既然編譯器知道了這么多信息,干嘛還要勞煩autorelease這個開銷不小的機制呢?于是乎,runtime使用了一些黑魔法將這個問題解決了,繼續(xù)往下看。

黑魔法之Thread Local Storage

Thread Local Storage(TLS)線程局部存儲,目的很簡單,將一塊內(nèi)存作為某個線程專有的存儲,以key-value的形式進行讀寫.
在返回值身上調(diào)用objc_autoreleaseReturnValue方法時,runtime將這個返回值object儲存在TLS中,然后直接返回這個object(不調(diào)用autorelease);同時,在外部接收這個返回值的objc_retainAutoreleasedReturnValue里,發(fā)現(xiàn)TLS中正好存了這個對象,那么直接返回這個object(不調(diào)用retain)。
于是乎,調(diào)用方和被調(diào)方利用TLS做中轉(zhuǎn),很有默契的免去了對返回值的內(nèi)存管理。這樣就省去了兩個操作:autorelease和外部的一次retain操作,對于性能提高很多。

自動釋放池用途

for (int i = 0; i < 100000000; i++)
{
    @autoreleasepool
    {
        NSString* string = @"ab c";
        NSArray* array = [string componentsSeparatedByString:string];   //放入自動釋放池中,從而避免大量局部變量要等待runloop將要休眠才釋放從而造成的內(nèi)存峰值。
    }
}

參考文章:
http://m.itdecent.cn/p/8b011b844231
https://blog.sunnyxx.com/2014/10/15/behind-autorelease/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容