探究自動釋放池的實現

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

Autorelease 概覽

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

    NSMutableArray *array = [NSMutableArray array];

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

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

AutoreleasePoolPage 的結構

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

page_dlist.png

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

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

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

 #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 不是獨立的結構,就要依靠這個哨兵來區(qū)分各個 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 對象實例化以及發(fā)送 -drain 消息時調用镇防。前者的調用最終落實到 AutoreleasePool 的靜態(tài)方法中:

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() 進去,滿了就續(xù)一個新 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
    // 這里省略了刪除空子節(jié)點的代碼
}

上面描述了 NSAutoreleasePool 在創(chuàng)建和傾倒時的具體工作過程,那么在給一個 Objective-C 對象發(fā)送 -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 下有效的狂丝、用于在發(fā)送 -autorelease 消息快速返回的機制换淆,編譯器根據相關的信息,決定是否要把一個對象放到 pool 中几颜,我應該會在探究 ARC 實現的時候寫這個東西倍试,現在感興趣的話可以去 sunnyxx 的《黑幕背后的Autorelease》了解相關信息。

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末沪蓬,一起剝皮案震驚了整個濱河市彤钟,隨后出現的幾起案子,更是在濱河造成了極大的恐慌跷叉,老刑警劉巖逸雹,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異性芬,居然都是意外死亡峡眶,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門植锉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辫樱,“玉大人,你說我怎么就攤上這事俊庇∈ㄊ睿” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵辉饱,是天一觀的道長搬男。 經常有香客問我,道長彭沼,這世上最難降的妖魔是什么缔逛? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上褐奴,老公的妹妹穿的比我還像新娘按脚。我一直安慰自己,他們只是感情好敦冬,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布辅搬。 她就那樣靜靜地躺著,像睡著了一般脖旱。 火紅的嫁衣襯著肌膚如雪堪遂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天萌庆,我揣著相機與錄音溶褪,去河邊找鬼。 笑死踊兜,一個胖子當著我的面吹牛竿滨,可吹牛的內容都是我干的。 我是一名探鬼主播捏境,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼于游,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了垫言?” 一聲冷哼從身側響起贰剥,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎筷频,沒想到半個月后蚌成,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡凛捏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年担忧,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坯癣。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡瓶盛,死狀恐怖,靈堂內的尸體忽然破棺而出示罗,到底是詐尸還是另有隱情惩猫,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布蚜点,位于F島的核電站轧房,受9級特大地震影響,放射性物質發(fā)生泄漏绍绘。R本人自食惡果不足惜奶镶,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一迟赃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧厂镇,春花似錦捺氢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悠反。三九已至残黑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間斋否,已是汗流浹背梨水。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留茵臭,地道東北人疫诽。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像旦委,于是被迫代替她去往敵國和親奇徒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容