上一篇依靠 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é)點,構成的雙向鏈表這樣的數據結構奸远。用一張圖來快速過一下它吧:
圖中有兩個 AutoreleasePoolPage
對象(以下簡稱 page 對象)既棺,每一個都是虛擬內存頁面的大小,除去底部(低地址)為 page 對象的成員變量所占的空間之外然走,剩余的內存空間看作一個棧援制,每個幀用來存儲將要被釋放的對象或者哨兵對象(用于區(qū)分 Pool 的邊界)。next
其實也是 page 對象的成員變量芍瑞,單獨畫出來是為了描述它作用:next
總是指向下一個可放入 id 對象的地址晨仑,直到棧被堆滿后指向棧頂。而 hotPage
不是成員變量拆檬,它是通過 TLS (Thread Local Storage)與線程綁定的處于活躍狀態(tài)的 page 對象洪己,這個說明了兩點:
- 多個線程直接不共享 page 對象,在多線程中使用 MRR 時要注意這個問題竟贯,免得對象多次被釋放或未能完成釋放答捕;
- 凡是增加或刪除對象都從這個活躍的 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 塊:
接著是一個 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多線程和內存管理》 這本書,雖然里面有些內容過時了谆趾,但是里面的探究原理的思路非常地清晰躁愿,結合實際去學習還是很有趣味的。 :)