黑幕背后的Autorelease

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

|

這個實驗同時也證明了viewDidLoadviewWillAppear是在同一個runloop調(diào)用的午乓,而viewDidAppear是在之后的某個runloop調(diào)用的。
由于這個vc在loadView之后便add到了window層級上闸准,所以viewDidLoadviewWillAppear是在同一個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)的類

image
  • 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)存如下圖:

image

圖中的情況斩狱,這一頁再加入一個autorelease對象就要滿了(也就是next指針馬上指向棧頂)耳高,這時就要執(zhí)行上面說的操作,建立下一頁page對象所踊,與這一頁鏈表連接完成后泌枪,新page的next指針被初始化在棧底(begin的位置),然后繼續(xù)向棧頂添加新對象秕岛。

所以碌燕,向一個對象發(fā)送- autorelease消息,就是將這個對象加入到當前AutoreleasePoolPage的棧頂next指針指向的位置

釋放時刻

每當進行一次objc_autoreleasePoolPush調(diào)用時继薛,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象修壕,值為0(也就是個nil),那么這一個page就變成了下面的樣子:

image

objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址遏考,被objc_autoreleasePoolPop(哨兵對象)作為入?yún)⒋瑞谑牵?/p>

  1. 根據(jù)傳入的哨兵對象地址找到哨兵對象所處的page
  2. 在當前page中,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次- release消息灌具,并向回移動next指針到正確位置
  3. 補充2:從最新加入的對象一直向前清理青团,可以向前跨越若干個page,直到哨兵所在的page

剛才的objc_autoreleasePoolPop執(zhí)行后咖楣,最終變成了下面的樣子:

image

嵌套的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>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纽疟,一起剝皮案震驚了整個濱河市罐韩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌污朽,老刑警劉巖散吵,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蟆肆,居然都是意外死亡矾睦,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門炎功,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枚冗,“玉大人,你說我怎么就攤上這事蛇损×尬拢” “怎么了坛怪?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長股囊。 經(jīng)常有香客問我袜匿,道長,這世上最難降的妖魔是什么稚疹? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任居灯,我火速辦了婚禮,結(jié)果婚禮上内狗,老公的妹妹穿的比我還像新娘穆壕。我一直安慰自己,他們只是感情好其屏,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缨该,像睡著了一般偎行。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贰拿,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天蛤袒,我揣著相機與錄音,去河邊找鬼膨更。 笑死妙真,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的荚守。 我是一名探鬼主播珍德,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼矗漾!你這毒婦竟也來了锈候?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤敞贡,失蹤者是張志新(化名)和其女友劉穎泵琳,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體誊役,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡获列,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛔垢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片击孩。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖啦桌,靈堂內(nèi)的尸體忽然破棺而出溯壶,到底是詐尸還是另有隱情及皂,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布且改,位于F島的核電站验烧,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏又跛。R本人自食惡果不足惜碍拆,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望慨蓝。 院中可真熱鬧感混,春花似錦礼烈、人聲如沸弧满。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庭呜。三九已至,卻和暖如春犀忱,著一層夾襖步出監(jiān)牢的瞬間募谎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工阴汇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留数冬,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓搀庶,卻偏偏與公主長得像拐纱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子地来,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容