是否似曾相識?
一优炬、簡介
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)的類
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對象地址時內存如下圖:
圖中的情況,這一頁再加入一個autorelease對象就要滿了(也就是next指針馬上指向棧頂)懈玻,這時就要執(zhí)行上面說的操作巧婶,建立下一頁page對象,與這一頁鏈表連接完成后涂乌,新page的next指針被初始化在棧底(begin的位置)艺栈,然后繼續(xù)向棧頂添加新對象。
所以湾盒,向一個對象發(fā)送- autorelease消息湿右,就是將這個對象加入到當前AutoreleasePoolPage的棧頂next指針指向的位置
釋放時刻
每當進行一次objc_autoreleasePoolPush調用時,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象罚勾,值為0(也就是個nil)诅需,那么這一個page就變成了下面的樣子:
objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop(哨兵對象)作為入參荧库,于是:
根據傳入的哨兵對象地址找到哨兵對象所處的page
在當前page中堰塌,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次- release消息,并向回移動next指針到正確位置
補充2:從最新加入的對象一直向前清理分衫,可以向前跨越若干個page场刑,直到哨兵所在的page
剛才的objc_autoreleasePoolPop執(zhí)行后,最終變成了下面的樣子:
嵌套的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)系,鄙人及時修改,多多包涵吼具!