iOS開發(fā)基礎性知識(九)----Autorelease

是否似曾相識?


mrc

一优炬、簡介

Autorelease機制是iOS開發(fā)者管理對象內存的好伙伴颁井,MRC中,調用[obj autorelease]來延遲內存的釋放是一件簡單自然的事蠢护,ARC下雅宾,我們甚至可以完全不知道Autorelease就能管理好內存。而在這背后葵硕,objc和編譯器都幫我們做了哪些事呢眉抬,它們是如何協(xié)作來正確管理內存的呢?刨根問底懈凹,一起來探究下黑幕背后的Autorelease機制蜀变。

二、Autorelease對象什么時候釋放介评?

這個問題拿來做面試題库北,問過很多人,沒有幾個能答對的们陆。很多答案都是“當前作用域大括號結束時釋放”寒瓦,顯然木有正確理解Autorelease機制。

在沒有手加Autorelease Pool的情況下坪仇,Autorelease對象是在當前的runloop迭代結束時釋放的杂腰,而它能夠釋放的原因是系統(tǒng)在每個runloop迭代中都加入了自動釋放池Push和Pop

測試代碼:

__weakidreference =nil;

- (void)viewDidLoad {

[superviewDidLoad];

NSString*str = [NSStringstringWithFormat:@"sunnyxx"];

// str是一個autorelease對象,設置一個weak的引用來觀察它

reference = str;

}

- (void)viewWillAppear:(BOOL)animated {

[superviewWillAppear:animated];

NSLog(@"%@", reference);// Console: sunnyxx

}

- (void)viewDidAppear:(BOOL)animated {

[superviewDidAppear:animated];

NSLog(@"%@", reference);// Console: (null)

}

這個實驗同時也證明了viewDidLoad和viewWillAppear是在同一個runloop調用的椅文,二viewDidAppear是在之后的某個runloop調用的喂很。

由于這個vc在loadView之后便add到了window層級上惜颇,所以viewDidLoad和viewWillAppear是在同一個runloop調用的,因此在viewWillAppear中恤筛,這個autorelease的變量依然有值官还。

當然,我們也可以手動干預Autorelease對象的釋放時機:

- (void)viewDidLoad {

[superviewDidLoad];

@autoreleasepool{

NSString*str = [NSStringstringWithFormat:@"sunnyxx"];

}

NSLog(@"%@", str);// Console: (null)

}

三毒坛、Autorelease原理
AutoreleasePoolPage

ARC下望伦,我們使用@autoreleasepool{}來使用一個AutoreleasePool,隨后編譯器將其改寫成下面的樣子:

void *context = objc_autoreleasePoolPush();

// {}中的代碼

objc_autoreleasePoolPop(context);

而這兩個函數(shù)都是對AutoreleasePoolPage的簡單封裝煎殷,所以自動釋放機制的核心就在于這個類屯伞。

AutoreleasePoolPage是一個C++實現(xiàn)的類


AutoreleasePoolPage

1.AutoreleasePool并沒有單獨的結構,而是由若干個AutoreleasePoolPage以雙向鏈表的形式組合而成(分別對應結構中的parent指針和child指針)

2.AutoreleasePool是按線程一一對應的(結構中的thread指針指向當前線程)

3.AutoreleasePoolPage每個對象會開辟4096字節(jié)內存(也就是虛擬內存一頁的大泻乐薄)劣摇,除了上面的實例變量所占空間,剩下的空間全部用來儲存autorelease對象的地址

4.上面的id *next指針作為游標指向棧頂最新add進來的autorelease對象的下一個位置

5.一個AutoreleasePoolPage的空間被占滿時弓乙,會新建一個AutoreleasePoolPage對象末融,連接鏈表,后來的autorelease對象在新的page加入

所以暇韧,若當前線程中只有一個AutoreleasePoolPage對象勾习,并記錄了很多autorelease對象地址時內存如下圖:

AutoreleasePoolPage2

圖中的情況,這一頁再加入一個autorelease對象就要滿了(也就是next指針馬上指向棧頂)懈玻,這時就要執(zhí)行上面說的操作巧婶,建立下一頁page對象,與這一頁鏈表連接完成后涂乌,新page的next指針被初始化在棧底(begin的位置)艺栈,然后繼續(xù)向棧頂添加新對象。

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

釋放時刻

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

objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop(哨兵對象)作為入參荧库,于是:


AutoreleasePoolPage3

根據傳入的哨兵對象地址找到哨兵對象所處的page

在當前page中堰塌,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次- release消息,并向回移動next指針到正確位置

補充2:從最新加入的對象一直向前清理分衫,可以向前跨越若干個page场刑,直到哨兵所在的page

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

AutoreleasePoolPage4

嵌套的AutoreleasePool

知道了上面的原理,嵌套的AutoreleasePool就非常簡單了牵现,pop的時候總會釋放到上次push的位置為止铐懊,多層的pool就是多個哨兵對象而已,就像剝洋蔥一樣瞎疼,每次一層科乎,互不影響。

【附加內容】

Autorelease返回值的快速釋放機制

值得一提的是贼急,ARC下茅茂,runtime有一套對autorelease返回值的優(yōu)化策略。

比如一個工廠方法

+ (instancetype)createSark {

return[selfnew];

}

// caller

Sark *sark = [Sark createSark];

秉著誰創(chuàng)建誰釋放的原則太抓,返回值需要是一個autorelease對象才能配合調用方正確管理內存空闲,于是乎編譯器改寫成了形如下面的代碼:

+ (instancetype)createSark {

idtmp = [selfnew];

returnobjc_autoreleaseReturnValue(tmp);// 代替我們調用autorelease

}

// caller

idtmp = objc_retainAutoreleasedReturnValue([Sark createSark])// 代替我們調用retain

Sark *sark = tmp;

objc_storeStrong(&sark,nil);// 相當于代替我們調用了release

一切看上去都很好,不過既然編譯器知道了這么多信息走敌,干嘛還要勞煩autorelease這個開銷不小的機制呢碴倾?于是乎,runtime使用了一些黑魔法將這個問題解決了掉丽。

黑魔法之Thread Local Storage

Thread Local Storage(TLS)線程局部存儲跌榔,目的很簡單,將一塊內存作為某個線程專有的存儲捶障,以key-value的形式進行讀寫矫户,比如在非arm架構下,使用pthread提供的方法實現(xiàn):

void* pthread_getspecific(pthread_key_t);

intpthread_setspecific(pthread_key_t ,constvoid*);

說它是黑魔法可能被懂pthread的笑話- -

在返回值身上調用objc_autoreleaseReturnValue方法時残邀,runtime將這個返回值object儲存在TLS中,然后直接返回這個object(不調用autorelease)柑蛇;同時芥挣,在外部接收這個返回值的objc_retainAutoreleasedReturnValue里,發(fā)現(xiàn)TLS中正好存了這個對象耻台,那么直接返回這個object(不調用retain)空免。

于是乎,調用方和被調方利用TLS做中轉盆耽,很有默契的免去了對返回值的內存管理蹋砚。

于是問題又來了,假如被調方和主調方只有一邊是ARC環(huán)境編譯的該咋辦摄杂?(比如我們在ARC環(huán)境下用了非ARC編譯的第三方庫坝咐,或者反之)

只能動用更高級的黑魔法。

黑魔法之__builtin_return_address

這個內建函數(shù)原型是char *__builtin_return_address(int level)析恢,作用是得到函數(shù)的返回地址墨坚,參數(shù)表示層數(shù),如__builtin_return_address(0)表示當前函數(shù)體返回地址映挂,傳1是調用這個函數(shù)的外層函數(shù)的返回值地址泽篮,以此類推盗尸。

- (int)foo {

NSLog(@"%p", __builtin_return_address(0));// 根據這個地址能找到下面ret的地址

return1;

}

// caller

intret = [sark foo];

看上去也沒啥厲害的,不過要知道帽撑,函數(shù)的返回值地址泼各,也就對應著調用者結束這次調用的地址(或者相差某個固定的偏移量,根據編譯器決定)

也就是說亏拉,被調用的函數(shù)也有翻身做地主的機會了扣蜻,可以反過來對主調方干點壞事。

回到上面的問題专筷,如果一個函數(shù)返回前知道調用方是ARC還是非ARC弱贼,就有機會對于不同情況做不同的處理

黑魔法之反查匯編指令

通過上面的__builtin_return_address加某些偏移量,被調方可以定位到主調方在返回值后面的匯編指令:

// caller

intret = [sark foo];

// 內存中接下來的匯編指令(x86磷蛹,我不懂匯編吮旅,瞎寫的)

movq ??? ???

callq ???

而這些匯編指令在內存中的值是固定的,比如movq對應著0x48味咳。

于是乎庇勃,就有了下面的這個函數(shù),入參是調用方__builtin_return_address傳入值

staticboolcallerAcceptsFastAutorelease(constvoid*constra0) {

constuint8_t *ra1 = (constuint8_t *)ra0;

constuint16_t *ra2;

constuint32_t *ra4 = (constuint32_t *)ra1;

constvoid**sym;

// 48 89 c7? ? movq? %rax,%rdi

// e8? ? ? ? ? callq symbol

if(*ra4 !=0xe8c78948) {

returnfalse;

}

ra1 += (long)*(constint32_t *)(ra1 +4) +8l;

ra2 = (constuint16_t *)ra1;

// ff 25? ? ? jmpq *symbol@DYLDMAGIC(%rip)

if(*ra2 !=0x25ff) {

returnfalse;

}

ra1 +=6l + (long)*(constint32_t *)(ra1 +2);

sym = (constvoid**)ra1;

if(*sym != objc_retainAutoreleasedReturnValue)

{

returnfalse;

}

returntrue;

}

它檢驗了主調方在返回值之后是否緊接著調用了objc_retainAutoreleasedReturnValue槽驶,如果是责嚷,就知道了外部是ARC環(huán)境,反之就走沒被優(yōu)化的老邏輯掂铐。

其他Autorelease相關知識點

使用容器的block版本的枚舉器時罕拂,內部會自動添加一個AutoreleasePool:

[array enumerateObjectsUsingBlock:^(idobj,NSUIntegeridx,BOOL*stop) {

// 這里被一個局部@autoreleasepool包圍著

}];

當然,在普通for循環(huán)和for in循環(huán)中沒有全陨,所以爆班,還是新版的block版本枚舉器更加方便。for循環(huán)中遍歷產生大量autorelease變量時辱姨,就需要手加局部AutoreleasePool了柿菩。




站在巨人的肩膀上才有這些總結

菜鳥走向大牛,大家共同前進雨涛,如果覺得不錯枢舶,請給個贊/關注。

一起交流學習替久,有問題隨時歡迎聯(lián)系凉泄,郵箱:383708669@qq.com

由于資料很早之前整理的了,現(xiàn)在找不到原來的出處蚯根,如對大神們有冒犯之舉旧困,還請聯(lián)系,鄙人及時修改,多多包涵吼具!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末僚纷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拗盒,更是在濱河造成了極大的恐慌怖竭,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陡蝇,死亡現(xiàn)場離奇詭異痊臭,居然都是意外死亡,警方通過查閱死者的電腦和手機登夫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門广匙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恼策,你說我怎么就攤上這事鸦致。” “怎么了涣楷?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵分唾,是天一觀的道長。 經常有香客問我狮斗,道長绽乔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任碳褒,我火速辦了婚禮折砸,結果婚禮上,老公的妹妹穿的比我還像新娘沙峻。我一直安慰自己睦授,他們只是感情好,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布专酗。 她就那樣靜靜地躺著,像睡著了一般盗扇。 火紅的嫁衣襯著肌膚如雪祷肯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天疗隶,我揣著相機與錄音佑笋,去河邊找鬼。 笑死斑鼻,一個胖子當著我的面吹牛蒋纬,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼蜀备,長吁一口氣:“原來是場噩夢啊……” “哼关摇!你這毒婦竟也來了?” 一聲冷哼從身側響起碾阁,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤输虱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后脂凶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宪睹,經...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年蚕钦,在試婚紗的時候發(fā)現(xiàn)自己被綠了亭病。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡嘶居,死狀恐怖罪帖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情食听,我是刑警寧澤胸蛛,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站樱报,受9級特大地震影響葬项,放射性物質發(fā)生泄漏。R本人自食惡果不足惜迹蛤,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一民珍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧盗飒,春花似錦嚷量、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宣渗,卻和暖如春抖所,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背痕囱。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工田轧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鞍恢。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓傻粘,卻偏偏與公主長得像每窖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子弦悉,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內容