AutoreleasePool的原理和實現

一、AutoreleasePool是什么

AutoreleasePool(自動釋放池)是OC中的一種內存自動回收機制,它可以延遲加入AutoreleasePool中的變量release的時機。在正常情況下,創建的變量會在超出其作用域的時候release,但是如果將變量加入AutoreleasePool,那么release將延遲執行。
看到這里有人可能會問,那到底延遲到什么時候執行呢?看完本文后,各位心中自然會有答案。

讓我們寫個Demo來驗證一下:

#import <Foundation/Foundation.h>

// 生成兩個全局weak變量用來觀察實驗對象
__weak NSString *weak_String;
__weak NSString *weak_StringAutorelease;

void createString(void) {
    
    NSString *string = [[NSString alloc] initWithFormat:@"Hello, World!"];    // 創建常規對象
    NSString *stringAutorelease = [NSString stringWithFormat:@"Hello, World! Autorelease"]; // 創建autorelease對象
    
    weak_String = string;
    weak_StringAutorelease = stringAutorelease;
    
    NSLog(@"------in the createString()------");
    NSLog(@"%@", weak_String);
    NSLog(@"%@\n\n", weak_StringAutorelease);
}

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        createString();
        NSLog(@"------in the autoreleasepool------");
        NSLog(@"%@", weak_String);
        NSLog(@"%@\n\n", weak_StringAutorelease);
    }
    NSLog(@"------in the main()------");
    NSLog(@"%@", weak_String);
    NSLog(@"%@", weak_StringAutorelease);
 
    return 0;
}

上述代碼運行結果如下:

2016-04-01 16:21:46.961 AutoreleasePool[18401:708414] ------in the createString()------
2016-04-01 16:21:46.962 AutoreleasePool[18401:708414] Hello, World!
2016-04-01 16:21:46.962 AutoreleasePool[18401:708414] Hello, World! Autorelease

2016-04-01 16:21:46.962 AutoreleasePool[18401:708414] ------in the autoreleasepool------
2016-04-01 16:21:46.962 AutoreleasePool[18401:708414] (null)
2016-04-01 16:21:46.962 AutoreleasePool[18401:708414] Hello, World! Autorelease

2016-04-01 16:21:46.962 AutoreleasePool[18401:708414] ------in the main()------
2016-04-01 16:21:46.962 AutoreleasePool[18401:708414] (null)
2016-04-01 16:21:46.962 AutoreleasePool[18401:708414] (null)
Program ended with exit code: 0

首先在createString函數中創建了一個常規NSString對象和一個autorelease對象,然后分別賦值給兩個weak全局變量用于觀察目標對象。通過兩個weak全局變量的打印結果我們可以看到,在createString函數中兩個對象都是正常存在的,出了createString函數在autoreleasepool中,常規對象已經被釋放,而autorelease對象依然存在。在autoreleasepool外,autorelease對象也被釋放了。

通過運行結果,我們已經直觀的了解了AutoreleasePool的作用,那么AutoreleasePool是如何實現的呢?

二、AutoreleasePool的實現

接下來我們將一步步探尋AutoreleasePool的底層實現:
首先我們調整上面的代碼,只留下main函數和@autoreleasepool{}。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
 
    return 0;
}

然后在終端中使用clang -rewrite-objc命令將上述OC代碼重寫成C++的實現。
搜索main我們可以看到main函數的實現重寫成了如下代碼:

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

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
    }

    return 0;
}

通過對比可以發現,蘋果通過聲明一個__AtAutoreleasePool類型的局部變量__autoreleasepool實現了@autoreleasepool{}。那么這一切是如何實現的呢?這就要看看__AtAutoreleasePool的定義了:

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

根據構造函數和析構函數的特點(自動局部變量的構造函數是在程序執行到聲明這個對象的位置時調用的,而對應的析構函數是在程序執行到離開這個對象的作用域時調用),我們可以將上面兩段代碼簡化成如下形式:

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

    /* @autoreleasepool */ {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

    return 0;
}

至此,我們可以分析出,單個自動釋放池的執行過程就是objc_autoreleasePoolPush() —> [object autorelease] —> objc_autoreleasePoolPop(void *)

看到這兩個函數的前綴,我們就知道它們是runtime中的兩個函數,接下來我們就打開runtime的源碼,看看它們是如何實現的。文中使用的源碼是objc4-680.tar.gz

三、AutoreleasePool源碼解析

在runtime項目中搜索objc_autoreleasePoolPush我們可以在objc/Source/NSObject.mm中的1749~1754行找到objc_autoreleasePoolPush()函數的實現:

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

同樣我們可以找到objc_autoreleasePoolPop()函數的實現:

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

看到這里,我們發現這兩個函數的實現都是調用了AutoreleasePoolPage類中的方法。于是我們可以斷定,AutoreleasePool的是通過AutoreleasePoolPage類來實現的。

打開AutoreleasePoolPage的定義我們可以看到它有下列屬性:

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

通過這些屬性,我們可以推斷出,這是一個雙向鏈表的節點,AutoreleasePool的內存結構就是一個雙向鏈表。而源碼上的注釋也證實我們的推測:

/***********************************************************************
   Autorelease pool implementation

   A thread's autorelease pool is a stack of pointers. 
   Each pointer is either an object to release, or POOL_SENTINEL which is 
     an autorelease pool boundary.
   A pool token is a pointer to the POOL_SENTINEL 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. 
**********************************************************************/

一個線程的autoreleasepool就是一個指針棧。
棧中存放的指針指向加入需要release的對象或者POOL_SENTINEL(哨兵對象,用于分隔autoreleasepool)。
棧中指向POOL_SENTINEL的指針就是autoreleasepool的一個標記。當autoreleasepool進行出棧操作,每一個比這個哨兵對象后進棧的對象都會release。
這個棧是由一個以page為節點雙向鏈表組成,page根據需求進行增減。
autoreleasepool對應的線程存儲了指向最新page(也就是最新添加autorelease對象的page)的指針。

通過閱讀源碼,我們可以分析出上述屬性的作用:

  • magic:用來校驗 AutoreleasePoolPage 的結構是否完整;
  • next:指向棧頂,也就是最新入棧的autorelease對象的下一個位置;
  • thread:指向當前線程;
  • parent:指向父節點
  • child:指向子節點
  • depth:表示鏈表的深度,也就是鏈表節點的個數
  • hiwat:表示high water mark(最高水位標記)

接下來我們看看實現AutoreleasePool的幾個關鍵函數是如何實現的。為了方便起見,就直接將注釋添加在代碼中。

AutoreleasePoolPage::push()

   static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {    // 區別調試模式
            // Each autorelease pool starts on a new pool page.
            // 調試模式下將新建一個鏈表節點,并將一個哨兵對象添加到鏈表棧中
            dest = autoreleaseNewPage(POOL_SENTINEL);
        } else {
            dest = autoreleaseFast(POOL_SENTINEL);    // 添加一個哨兵對象到自動釋放池的鏈表棧中
        }
        assert(*dest == POOL_SENTINEL);
        return dest;
    }
    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();    // 獲取最新的page(即鏈表上最新的節點)
        if (page && !page->full()) {
            return page->add(obj);    // 在這個page存在且不滿的情況下,直接將需要autorelease的對象加入棧中
        } else if (page) {
            return autoreleaseFullPage(obj, page);    // 在這個page已經滿了的情況下,新建一個page并將obj對象放入新的page(即入棧)
        } else {
            return autoreleaseNoPage(obj);    // 在沒有page的情況下,新建一個page并將obj對象放入新的page(即入棧)
        }
    }

autoreleaseFullPage(obj, page)autoreleaseNoPage(obj)的區別在于autoreleaseFullPage(obj, page)會將當前page的child指向新建的page,而autoreleaseNoPage(obj)會在新建的page中先入棧一個POOL_SENTINEL(哨兵對象),再將obj入棧。

    id *add(id obj)   // 入棧操作
    {
        assert(!full());
        unprotect();    // 解除保護
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;    // 將obj入棧到棧頂并重新定位棧頂
        protect();    // 添加保護
        return ret;
    }

AutoreleasePoolPage::pop(ctxt);

    static inline void pop(void *token)   // token指針指向棧頂的地址
    {
        AutoreleasePoolPage *page;
        id *stop;

        page = pageForPointer(token);   // 通過棧頂的地址找到對應的page
        stop = (id *)token;
        if (DebugPoolAllocation  &&  *stop != POOL_SENTINEL) {
            // This check is not valid with DebugPoolAllocation off
            // after an autorelease with a pool page but no pool in place.
            _objc_fatal("invalid or prematurely-freed autorelease pool %p; ", 
                        token);
        }

        if (PrintPoolHiwat) printHiwat();   // 記錄最高水位標記

        page->releaseUntil(stop);   // 從棧頂開始操作出棧,并向棧中的對象發送release消息,直到遇到第一個哨兵對象

        // memory: delete empty children
        // 刪除空掉的節點
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (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();
            }
        }
    }

AutoreleasePoolPage::autorelease((id)this);

    static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);   //  添加obj對象到自動釋放池的鏈表棧中
        assert(!dest  ||  *dest == obj);
        return obj;
    }

autorelease函數和push函數一樣,關鍵代碼都是調用autoreleaseFast函數向自動釋放池的鏈表棧中添加一個對象,不過push函數的入棧的是一個哨兵對象,而autorelease函數入棧的是需要加入autoreleasepool的對象。

四、補充

上面我們講了AutoreleasePoolPage的定義的屬性中有一個hiwat表示high water mark(最高水位標記)。那么什么是最高水位標記呢?這個概念可以用自然界中的潮汐現象來解釋。大家都知道潮水是有漲有落的,漲潮漲到最高時的水位就是最高水位。放在代碼中來說,autoreleasepool的內存結構是一個雙向鏈表棧,會頻繁的有入棧和出棧操作,棧中存放的對象也會有增有減,hiwat就記錄了入棧對象最多時候對象的個數。

    static void printHiwat()
    {
        // Check and propagate high water mark
        // Ignore high water marks under 256 to suppress noise.
        AutoreleasePoolPage *p = hotPage();   // 獲取最新的page
        uint32_t mark = p->depth*COUNT + (uint32_t)(p->next - p->begin());    // 計算棧中對象的數量
        if (mark > p->hiwat  &&  mark > 256) {    // 當數量大于當前記錄的最高水位標記且大
            for( ; p; p = p->parent) {            // 于256,更新每個page中的最高水位標記
                p->unprotect();
                p->hiwat = mark;
                p->protect();
            }
            
            // ······
        }
    }

回到開頭的問題:加入AutoreleasePool的對象,release將延遲到什么時候執行?
相信現在各位心里都已經有答案了。

參考鏈接

http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
http://blog.leichunfeng.com/blog/2015/05/31/objective-c-autorelease-pool-implementation-principle/#jtss-tsina

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容