<article class="post-block" style="display: block;">
我是前言
Autorelease機制是iOS開發(fā)者管理對象內(nèi)存的好伙伴,MRC中徒扶,調(diào)用[obj autorelease]
來延遲內(nèi)存的釋放是一件簡單自然的事根穷,ARC下屿良,我們甚至可以完全不知道Autorelease就能管理好內(nèi)存管引。而在這背后褥伴,objc和編譯器都幫我們做了哪些事呢,它們是如何協(xié)作來正確管理內(nèi)存的呢饥臂?刨根問底隅熙,一起來探究下黑幕背后的Autorelease機制囚戚。
Autorelease對象什么時候釋放轧简?
這個問題拿來做面試題哮独,問過很多人皮璧,沒有幾個能答對的。很多答案都是“當前作用域大括號結(jié)束時釋放”睹限,顯然木有正確理解Autorelease機制邦泄。
在沒有手加Autorelease Pool的情況下顺囊,Autorelease對象是在當前的runloop
迭代結(jié)束時釋放的特碳,而它能夠釋放的原因是系統(tǒng)在每個runloop迭代中都加入了自動釋放池Push和Pop
小實驗
|
<pre style="overflow: auto; font-family: Menlo, "Roboto Mono", Monaco, courier, monospace; font-size: 14px; background-color: rgb(248, 248, 248); color: rgb(82, 82, 82); padding: 1.2em 1.4em; line-height: 1.5em; margin: 0px;">__weak id reference = nil;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = [NSString stringWithFormat:@"sunnyxx"];
// str是一個autorelease對象晕换,設(shè)置一個weak的引用來觀察它
reference = str;
} - (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", reference); // Console: sunnyxx
} - (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%@", reference); // Console: (null)
}
</pre>
|
這個實驗同時也證明了viewDidLoad
和viewWillAppear
是在同一個runloop調(diào)用的午乓,而viewDidAppear
是在之后的某個runloop調(diào)用的。
由于這個vc在loadView之后便add到了window層級上闸准,所以viewDidLoad
和viewWillAppear
是在同一個runloop調(diào)用的益愈,因此在viewWillAppear
中,這個autorelease的變量依然有值。
當然蒸其,我們也可以手動干預(yù)Autorelease對象的釋放時機:
|
<pre style="overflow: auto; font-family: Menlo, "Roboto Mono", Monaco, courier, monospace; font-size: 14px; background-color: rgb(248, 248, 248); color: rgb(82, 82, 82); padding: 1.2em 1.4em; line-height: 1.5em; margin: 0px;">- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"sunnyxx"];
}
NSLog(@"%@", str); // Console: (null)
}
</pre>
|
Autorelease原理
AutoreleasePoolPage
ARC下敏释,我們使用@autoreleasepool{}
來使用一個AutoreleasePool摸袁,隨后編譯器將其改寫成下面的樣子:
|
<pre style="overflow: auto; font-family: Menlo, "Roboto Mono", Monaco, courier, monospace; font-size: 14px; background-color: rgb(248, 248, 248); color: rgb(82, 82, 82); padding: 1.2em 1.4em; line-height: 1.5em; margin: 0px;">void *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);
</pre>
|
而這兩個函數(shù)都是對AutoreleasePoolPage
的簡單封裝钥顽,所以自動釋放機制的核心就在于這個類。
AutoreleasePoolPage是一個C++實現(xiàn)的類
- AutoreleasePool并沒有單獨的結(jié)構(gòu)靠汁,而是由若干個AutoreleasePoolPage以
雙向鏈表
的形式組合而成(分別對應(yīng)結(jié)構(gòu)中的parent指針和child指針) - AutoreleasePool是按線程一一對應(yīng)的(結(jié)構(gòu)中的thread指針指向當前線程)
- AutoreleasePoolPage每個對象會開辟4096字節(jié)內(nèi)存(也就是虛擬內(nèi)存一頁的大蟹浯蟆),除了上面的實例變量所占空間蝶怔,剩下的空間全部用來儲存autorelease對象的地址
- 上面的
id *next
指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置 - 一個AutoreleasePoolPage的空間被占滿時奶浦,會新建一個AutoreleasePoolPage對象,連接鏈表添谊,后來的autorelease對象在新的page加入
所以财喳,若當前線程中只有一個AutoreleasePoolPage對象,并記錄了很多autorelease對象地址時內(nèi)存如下圖:
圖中的情況斩狱,這一頁再加入一個autorelease對象就要滿了(也就是next指針馬上指向棧頂)耳高,這時就要執(zhí)行上面說的操作,建立下一頁page對象所踊,與這一頁鏈表連接完成后泌枪,新page的next
指針被初始化在棧底(begin的位置),然后繼續(xù)向棧頂添加新對象秕岛。
所以碌燕,向一個對象發(fā)送- autorelease
消息,就是將這個對象加入到當前AutoreleasePoolPage的棧頂next指針指向的位置
釋放時刻
每當進行一次objc_autoreleasePoolPush
調(diào)用時继薛,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象
修壕,值為0(也就是個nil),那么這一個page就變成了下面的樣子:
objc_autoreleasePoolPush
的返回值正是這個哨兵對象的地址遏考,被objc_autoreleasePoolPop(哨兵對象)
作為入?yún)⒋瑞谑牵?/p>
- 根據(jù)傳入的哨兵對象地址找到哨兵對象所處的page
- 在當前page中,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次
- release
消息灌具,并向回移動next
指針到正確位置 - 補充2:從最新加入的對象一直向前清理青团,可以向前跨越若干個page,直到哨兵所在的page
剛才的objc_autoreleasePoolPop執(zhí)行后咖楣,最終變成了下面的樣子:
嵌套的AutoreleasePool
知道了上面的原理督笆,嵌套的AutoreleasePool就非常簡單了,pop的時候總會釋放到上次push的位置為止诱贿,多層的pool就是多個哨兵對象而已娃肿,就像剝洋蔥一樣,每次一層,互不影響咸作。
【附加內(nèi)容】
Autorelease返回值的快速釋放機制
值得一提的是锨阿,ARC下,runtime有一套對autorelease返回值的優(yōu)化策略记罚。
比如一個工廠方法:
|
<pre style="overflow: auto; font-family: Menlo, "Roboto Mono", Monaco, courier, monospace; font-size: 14px; background-color: rgb(248, 248, 248); color: rgb(82, 82, 82); padding: 1.2em 1.4em; line-height: 1.5em; margin: 0px;">+ (instancetype)createSark {
return [self new];
}
// caller
Sark *sark = [Sark createSark];
</pre>
|
秉著誰創(chuàng)建誰釋放的原則,返回值需要是一個autorelease對象才能配合調(diào)用方正確管理內(nèi)存壳嚎,于是乎編譯器改寫成了形如下面的代碼:
|
<pre style="overflow: auto; font-family: Menlo, "Roboto Mono", Monaco, courier, monospace; font-size: 14px; background-color: rgb(248, 248, 248); color: rgb(82, 82, 82); padding: 1.2em 1.4em; line-height: 1.5em; margin: 0px;">+ (instancetype)createSark {
id tmp = [self new];
return objc_autoreleaseReturnValue(tmp); // 代替我們調(diào)用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我們調(diào)用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相當于代替我們調(diào)用了release
</pre>
|
一切看上去都很好桐智,不過既然編譯器知道了這么多信息,干嘛還要勞煩autorelease這個開銷不小的機制呢烟馅?于是乎说庭,runtime使用了一些黑魔法將這個問題解決了。
黑魔法之Thread Local Storage
Thread Local Storage(TLS)線程局部存儲郑趁,目的很簡單刊驴,將一塊內(nèi)存作為某個線程專有的存儲,以key-value的形式進行讀寫寡润,比如在非arm架構(gòu)下捆憎,使用pthread提供的方法實現(xiàn):
|
<pre style="overflow: auto; font-family: Menlo, "Roboto Mono", Monaco, courier, monospace; font-size: 14px; background-color: rgb(248, 248, 248); color: rgb(82, 82, 82); padding: 1.2em 1.4em; line-height: 1.5em; margin: 0px;">void* pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t , const void *);
</pre>
|
說它是黑魔法可能被懂pthread的笑話- -
在返回值身上調(diào)用objc_autoreleaseReturnValue
方法時,runtime將這個返回值object儲存在TLS中梭纹,然后直接返回這個object(不調(diào)用autorelease)躲惰;同時,在外部接收這個返回值的objc_retainAutoreleasedReturnValue
里变抽,發(fā)現(xiàn)TLS中正好存了這個對象础拨,那么直接返回這個object(不調(diào)用retain)。
于是乎绍载,調(diào)用方和被調(diào)方利用TLS做中轉(zhuǎn)诡宗,很有默契的免去了對返回值的內(nèi)存管理。
于是問題又來了击儡,假如被調(diào)方和主調(diào)方只有一邊是ARC環(huán)境編譯的該咋辦塔沃?(比如我們在ARC環(huán)境下用了非ARC編譯的第三方庫,或者反之)
只能動用更高級的黑魔法曙痘。
黑魔法之__builtin_return_address
這個內(nèi)建函數(shù)原型是char *__builtin_return_address(int level)
芳悲,作用是得到函數(shù)的返回地址,參數(shù)表示層數(shù)边坤,如__builtin_return_address(0)表示當前函數(shù)體返回地址名扛,傳1是調(diào)用這個函數(shù)的外層函數(shù)的返回值地址,以此類推茧痒。
|
<pre style="overflow: auto; font-family: Menlo, "Roboto Mono", Monaco, courier, monospace; font-size: 14px; background-color: rgb(248, 248, 248); color: rgb(82, 82, 82); padding: 1.2em 1.4em; line-height: 1.5em; margin: 0px;">- (int)foo {
NSLog(@"%p", __builtin_return_address(0)); // 根據(jù)這個地址能找到下面ret的地址
return 1;
}
// caller
int ret = [sark foo];
</pre>
|
看上去也沒啥厲害的肮韧,不過要知道,函數(shù)的返回值地址,也就對應(yīng)著調(diào)用者結(jié)束這次調(diào)用的地址(或者相差某個固定的偏移量弄企,根據(jù)編譯器決定)
也就是說超燃,被調(diào)用的函數(shù)也有翻身做地主的機會了,可以反過來對主調(diào)方干點壞事拘领。
回到上面的問題意乓,如果一個函數(shù)返回前知道調(diào)用方是ARC還是非ARC,就有機會對于不同情況做不同的處理
黑魔法之反查匯編指令
通過上面的__builtin_return_address加某些偏移量约素,被調(diào)方可以定位到主調(diào)方在返回值后面的匯編指令
:
|
<pre style="overflow: auto; font-family: Menlo, "Roboto Mono", Monaco, courier, monospace; font-size: 14px; background-color: rgb(248, 248, 248); color: rgb(82, 82, 82); padding: 1.2em 1.4em; line-height: 1.5em; margin: 0px;">// caller
int ret = [sark foo];
// 內(nèi)存中接下來的匯編指令(x86届良,我不懂匯編,瞎寫的)
movq ??? ???
callq ???
</pre>
|
而這些匯編指令在內(nèi)存中的值是固定的圣猎,比如movq對應(yīng)著0x48士葫。
于是乎,就有了下面的這個函數(shù)送悔,入?yún)⑹钦{(diào)用方__builtin_return_address傳入值
|
<pre style="overflow: auto; font-family: Menlo, "Roboto Mono", Monaco, courier, monospace; font-size: 14px; background-color: rgb(248, 248, 248); color: rgb(82, 82, 82); padding: 1.2em 1.4em; line-height: 1.5em; margin: 0px;">static bool callerAcceptsFastAutorelease(const void * const ra0) {
const uint8_t *ra1 = (const uint8_t *)ra0;
const uint16_t *ra2;
const uint32_t *ra4 = (const uint32_t *)ra1;
const void sym;
// 48 89 c7 movq %rax,%rdi
// e8 callq symbol
if (ra4 != 0xe8c78948) {
return false;
}
ra1 += (long)(const int32_t *)(ra1 + 4) + 8l;
ra2 = (const uint16_t )ra1;
// ff 25 jmpq symbol@DYLDMAGIC(%rip)
if (ra2 != 0x25ff) {
return false;
}
ra1 += 6l + (long)(const int32_t *)(ra1 + 2);
sym = (const void *)ra1;
if (sym != objc_retainAutoreleasedReturnValue)
{
return false;
}
return true;
}
</pre>
|
它檢驗了主調(diào)方在返回值之后是否緊接著調(diào)用了objc_retainAutoreleasedReturnValue
慢显,如果是,就知道了外部是ARC環(huán)境欠啤,反之就走沒被優(yōu)化的老邏輯荚藻。
其他Autorelease相關(guān)知識點
使用容器的block版本的枚舉器時,內(nèi)部會自動添加一個AutoreleasePool:
|
<pre style="overflow: auto; font-family: Menlo, "Roboto Mono", Monaco, courier, monospace; font-size: 14px; background-color: rgb(248, 248, 248); color: rgb(82, 82, 82); padding: 1.2em 1.4em; line-height: 1.5em; margin: 0px;">[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 這里被一個局部@autoreleasepool包圍著
}];
</pre>
|
當然跪妥,在普通for循環(huán)和for in循環(huán)中沒有鞋喇,所以,還是新版的block版本枚舉器更加方便眉撵。for循環(huán)中遍歷產(chǎn)生大量autorelease變量時侦香,就需要手加局部AutoreleasePool咯。
</article>