探究自動釋放池的實現

上一篇依靠 objc-runtime 的源碼學習了引用計數的原理和具體實現,但并沒有解釋內存管理法則第二條中的“非自己生成的對象”是如何被釋放的。要想回答這個問題,必須了解 AutoreleasePool 這個概念(討論的環境還是 MRR 而非 ARC)。

Autorelease 概覽

談到內存管理的第二條法則時,出現了使用非 allow/new/copy/mutableCopy 開頭的方法生成的對象,比如:

    NSMutableArray *array = [NSMutableArray array];

我們并沒有持有這個 array 對象,那我們也就沒有權利釋放它(當然你也可以釋放它,只是會導致程序崩潰而已)。既然我們不能去釋放它,那么我們就需要一套機制去做這個事情 —— Autorelease 就這種用于延遲釋放對象的一種機制。簡要地說,就是向對象發送 -autorelease 消息,將對象放到 AutoreleasePool 中,在某個時刻,向這個 Pool 中的所有對象發送 -release 消息。所以上面的 +array 方法的實現可能是這樣的:

    +(instancetype)array {
        return [[NSMutableArray new] autorelease]; 
    }

AutoreleasePoolPage 的結構

在談到 AutoreleasePool 時,我們會想象它是一個類似 Array 或者 Set 這樣的容器對象,其實不然。AutoreleasePool 的實現并不是建立在一個容器上的,而是依賴于由一個或多個的 AutoreleasePoolPage 對象作為節點,構成的雙向鏈表這樣的數據結構。用一張圖來快速過一下它吧:

page_dlist.png

圖中有兩個 AutoreleasePoolPage 對象(以下簡稱 page 對象),每一個都是虛擬內存頁面的大小,除去底部(低地址)為 page 對象的成員變量所占的空間之外,剩余的內存空間看作一個棧,每個幀用來存儲將要被釋放的對象或者哨兵對象(用于區分 Pool 的邊界)。next 其實也是 page 對象的成員變量,單獨畫出來是為了描述它作用:next 總是指向下一個可放入 id 對象的地址,直到棧被堆滿后指向棧頂。而 hotPage 不是成員變量,它是通過 TLS (Thread Local Storage)與線程綁定的處于活躍狀態的 page 對象,這個說明了兩點:

  1. 多個線程直接不共享 page 對象,在多線程中使用 MRR 時要注意這個問題,免得對象多次被釋放或未能完成釋放;
  2. 凡是增加或刪除對象都從這個活躍的 page 對象開始操作。

AutoreleasePoolPage 這個類的完整定義也在 NSObject.mm 這個文件里面,下面列舉主要的一些(靜態)成員變量,有好些我還不知道其作用,望指教:

 #define POOL_SENTINEL nil   // 哨兵對象
static pthread_key_t const key = AUTORELEASE_POOL_KEY;  // 用于 TLS 獲得 hotPage 的 key
static uint8_t const SCRIBBLE = 0xA3;  // 亂寫的數據,用于填充被釋放對象所占據的“幀”
static size_t const SIZE = PAGE_MAX_SIZE;  // 該類重載了 new 操作符,為 page 對象分配 SIZE 這么大的內存空間
static size_t const COUNT = SIZE / sizeof(id);

magic_t const magic;    // 應該是類似于魔數之類的東西,用于標記和判斷什么?
id *next;   // 能放置對象的下一個地址或棧頂
pthread_t const thread; // 與該 page 對象綁定的線程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;   // page 的深度,或者說是這個里鏈表頭部的距離,第一個結點為 0,第二個為 1,以此類推
uint32_t hiwat; // high water 高水位?不清楚其作用

首先是 POOL_SENTINEL,也就是剛剛提到哨兵對象,實際只是 nil 的別名而已。使用過 NSAutoreleasePool 的人都知道,Pool 是可以嵌套使用的,而在實現上,由于每個 Pool 不是獨立的結構,就要依靠這個哨兵來區分各個 Pool 塊:

embed_pool.png

接著是一個 pthread_key_t const key,這是用來獲得與線程綁定的數據的鍵,結合 pthread_setspecific()pthread_getspecific() 等函數,,讓每個線程都能擁有屬于自己的那一份看起來是全局變量的數據(比較典型的例子是 errno,在某個線程出現的錯誤不會覆蓋另一個線程的錯誤碼)。不熟悉的話,換做 Objective-C 的實現可能會好理解一點:

NSString *key = @"Error_Key";

NSMutableDictionary * dic = [[NSThread currentThread] threadDictionary];
[dic setObject:@"error in main thread" forKey:key];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSMutableDictionary * bgDic = [[NSThread currentThread] threadDictionary];
    [bgDic setObject:@"error in child thread" forKey:key];
    NSLog(@"error: %@", [bgDic objectForKey:key]);
});

sleep(1);
NSLog(@"error: %@", [dic objectForKey:key]);

顯然兩個線程的 threadDictionary 是獨立的。

AutoreleasePool 的工作流程

_objc_autoreleasePoolPush()_objc_autoreleasePoolPop() 這兩個函數,分別在 NSAutoreleasePool 對象實例化以及發送 -drain 消息時調用。前者的調用最終落實到 AutoreleasePool 的靜態方法中:

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

通常會跳進 autoreleaseFast() 中插入一個哨兵對象,并返回哨兵所在幀的地址給外部。

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

autoreleaseFast() 的邏輯非常簡單,沒有 hotPage 就新建一個,hotPage 沒滿就直接 add() 進去,滿了就一個新 page 對象,沒什么好說的。感興趣的話可以去讀一下源碼。

前面說到插入哨兵之后會返回一個幀的地址,這個地址作為參數傳遞給 _objc_autoreleasePoolPop(),表示釋放的終點。但是光知道終點是不夠的,你還得知道終點在哪個 page 對象上,才能讓 page 對象調用成員函數 releaseUntil()。所以就有了下面這個函數:

static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE;

    assert(offset >= sizeof(AutoreleasePoolPage));

    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();

    return result;
}

pageForPointer() 通過哨兵的地址 p 對頁面大小取余獲得偏移量,再用 p 減去偏移量,就是哨兵所在 page 對象的地址了。pop() 函數完成釋放工作(再啰嗦一下這個 token 是前面返回的哨兵所在的幀地址),當 page 對象調用 releaseUntil() 時,從 next 指針開始,往回釋放每個對象,直到 stop 這個地址。

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
    
    page = pageForPointer(token);
    stop = (id *)token;
    // 這里省略了提前釋放導致錯誤的代碼

    if (PrintPoolHiwat) printHiwat();
    
    page->releaseUntil(stop);

    // memory: delete empty children
    // 這里省略了刪除空子節點的代碼
}

上面描述了 NSAutoreleasePool 在創建和傾倒時的具體工作過程,那么在給一個 Objective-C 對象發送 -autorelease 消息會是怎么樣的呢?下面是其實現:

inline id 
objc_object::rootAutorelease()
{
    assert(!UseGC);
    if (isTaggedPointer())
        return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1))
        return (id)this;

    return rootAutorelease2();
}

上一篇講過 tagged pointer object 了,這里不再贅敘。prepareOptimizedReturn() 是在 ARC 下有效的、用于在發送 -autorelease 消息快速返回的機制,編譯器根據相關的信息,決定是否要把一個對象放到 pool 中,我應該會在探究 ARC 實現的時候寫這個東西,現在感興趣的話可以去 sunnyxx 的《黑幕背后的Autorelease》了解相關信息。

最后這個 rootAutorelease2() (這隨性的命名)會調用到前面說到過的 autoreleaseFast() 函數,將對象加入 pool 中。

最后的最后再推薦一下《Objective-C 高級編程 iOS與OS X多線程和內存管理》 這本書,雖然里面有些內容過時了,但是里面的探究原理的思路非常地清晰,結合實際去學習還是很有趣味的。 :)

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

推薦閱讀更多精彩內容