前言
hi,勇敢的小夥伴兒們大家好,疫情終於進入一個比較好的階段,我也在這段時間內經歷了很多,有了很多感悟,能在這場大自然的災難中活下來,都是極其幸運的人啊,致敬那些擋住黑暗的人,致敬如今的美好生活。
珍惜當下。感恩所有。
好了,不囉嗦了,今天爲大家分享的是AutoreleasePool這個ARC下的大功臣。
正文
一、AutoreleasePool是什麼?
AutoreleasePool是OC中的一種內存自動回收機制,它可以延遲加入AutoreleasePool中的變量的release的時機。
在正常情況下,創建的變量會在超出其作用域的時候release,但是如果將變量加入AutoreleasePool,那麼release將延遲執行。
具體用代碼來展示:
#import <Foundation/Foundation.h>
//生成兩個全局的weak變量用來觀察對象,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, char * argv[]) {
//AutoreleasePool
@autoreleasepool {
createString();
NSLog(@"------in the createString()------");
NSLog(@"%@", weak_String);
NSLog(@"%@\n\n", weak_StringAutorelease);
}
NSLog(@"------in the main()------");
NSLog(@"%@", weak_String);
NSLog(@"%@", weak_StringAutorelease);
return 0;
}
運行結果如下圖所示:
我們可以清楚的看到在creatString方法中,兩個對象都是正常存在的,但是在AutoreleasePool中的沒有Autorelease的對象已經被釋放掉了,而autorelease的對象仍然存在,在AutoreleasePool外,autorelease的對象也被釋放掉了。
由此我們可以清楚的看出AutoreleasePool對weak_StringAutorelease對象的延遲釋放。
二.實現
知道了AutoreleasePool的作用之後,也想知道他的實現吧。
那麼調整一下上面的代碼,只留下main函數和@autorelease{}
#import <Foundation/Foundation.h>
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"Hello World!");
}
return 0;
}
然後在終端中使用clang -rewrite-objc命令將上述代碼重寫成C++實現。
在Finder中找到main.cpp文件
我們可以看到main函數的實現被重寫成了如下圖所示的代碼:
其中folders後面的字符串會不相同,但是沒關係並不影響我們的分析。
通過兩個代碼塊的對比我們可以發現,蘋果通過聲明一個__AtAutoreleasePool類型的局部變量__autoreleasepool實現了@autoreleasepool{}。那麼這又是如何實現的呢?
這就要看看__AtAutoreleasePool的定義了:
根據構造函數(構造函數 ,是一種特殊的方法。主要用來在創建對象時初始化對象, 即爲對象成員變量賦初始值,總與new運算符一起使用在創建對象的語句中。)和析構函數(析構函數(destructor) 與構造函數相反,當對象結束其生命週期,如對象所在的函數已調用完畢時,系統自動執行析構函數。)的特點(自動局部變量的構造函數實在程序執行到聲明這個對象的位置時調用的,而對應的析構函數實在程序執行到離開這個對象的作用域時調用),我們可以將上面的兩段代碼簡化成以下形式:
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 *)。
看到這兩個函數的前綴objc_我們就知道他們是runtime中的函數,接下來我們就打開runtime源碼,看看他們是如何實現的。
三、源碼解析
在runtime objc-723版本源碼RuntimeAnalyze中查找objc_autoreleasePoolPush方法,如下圖所示:
在NSObjec.mm文件中我們找到objc_autoreleasePoolPush()函數的實現:
也找到了objc_autoreleasePoolPop()函數的實現:
看到這裏,我們發現這兩個函數的實現都調用了AutoreleasePoolPage類中的方法。
於是我們可以斷定,AutoreleasePool是通過AutoreleasePoolPage類實現的。
打開AutoreleasePoolPage類,我們可以看到它有以下屬性:
通過這些屬性,我們可以推斷出,這是一個雙向鏈表的節點,AutoreleasePool的內存結構就是一個雙向鏈表。
而源碼上的註釋也可以證實我們的推測:
英語不好也沒關係,谷歌翻譯輔助理解:
翻譯的不是很準確,但也基本可以看懂了。
一個線程的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();
autoreleaseFullPage(obj, page)
和autoreleaseNoPage(obj)
的區別在於autoreleaseFullPage(obj, page)
會將當前page的child指向新建的page,而autoreleaseNoPage(obj)
會在新建的page中先入棧一個POOL_SENTINEL
(哨兵對象),再將obj入棧。
AutoreleasePoolPage::pop(ctxt);
1134行左右
static inline void pop(void *token) //token指針指向棧頂的地址
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}
page = pageForPointer(token); //通過棧頂的地址找到對應的page
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(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);
autorelease
函數和push
函數一樣,關鍵代碼都是調用autoreleaseFast
函數向自動釋放池的鏈表棧中添加一個對象,不過push
函數的入棧的是一個哨兵對象,而autorelease
函數入棧的是需要加入autoreleasepool
的對象。
四、補充
上面講了AutoreleasePoolPage的定義屬性中有一個hiwat表示high water mark(最高水位標記)。那麼什麼是最高水位標記呢?本身AutoreleasePool就是個水池,水池總有其最高能盛水的量。既然AutoreleasePool的內存結構是一個雙向鏈表棧,會頻繁的有入棧和出棧的操作,棧中存放的對象也會有增有減,hiwat就是爲了記錄入棧對象最多時候對象的個數。
五、總結
看到最後,那些加入AutoreleasePool中的對象到底延遲到什麼時候釋放呢?
我覺得應該是在當前runloop迭代結束時釋放的,因爲系統在每個runloop迭代中都加入了自動釋放池push和pop。
如果有錯誤煩請指出,共同學習,共同進步!
參考鏈接:https://www.jianshu.com/p/1b66c4d47cd7 和 http://www.cnblogs.com/L-vincen/p/6596095.html