Autorelease機制是iOS開發者管理對象內存的好夥伴,MRC中,調用[obj autorelease]
來延遲內存的釋放是一件簡單天然的事,ARC下,咱們甚至能夠徹底不知道Autorelease就能管理好內存。而在這背後,objc和編譯器都幫咱們作了哪些事呢,它們是如何協做來正確管理內存的呢?刨根問底,一塊兒來探究下黑幕背後的Autorelease機制。原文連接面試
這個問題拿來作面試題,問過不少人,沒有幾個能答對的。不少答案都是「當前做用域大括號結束時釋放」,顯然木有正確理解Autorelease機制。
在沒有手加Autorelease Pool的狀況下,Autorelease對象是在當前的runloop
迭代結束時釋放的,而它可以釋放的緣由是系統在每一個runloop迭代中都加入了自動釋放池Push和Pop 架構
__weak id reference = nil; - (void)viewDidLoad { [super viewDidLoad]; NSString *str = [NSString stringWithFormat:@"sunnyxx"]; // str是一個autorelease對象,設置一個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) }
這個實驗同時也證實了viewDidLoad
和viewWillAppear
是在同一個runloop調用的,而viewDidAppear
是在以後的某個runloop調用的。
因爲這個vc在loadView以後便add到了window層級上,因此viewDidLoad
和viewWillAppear
是在同一個runloop調用的,所以在viewWillAppear
中,這個autorelease的變量依然有值。 函數
固然,咱們也能夠手動干預Autorelease對象的釋放時機: oop
- (void)viewDidLoad { [super viewDidLoad]; @autoreleasepool { NSString *str = [NSString stringWithFormat:@"sunnyxx"]; } NSLog(@"%@", str); // Console: (null) }
ARC下,咱們使用@autoreleasepool{}
來使用一個AutoreleasePool,隨後編譯器將其改寫成下面的樣子:優化
void *context = objc_autoreleasePoolPush(); // {}中的代碼 objc_autoreleasePoolPop(context);
而這兩個函數都是對AutoreleasePoolPage
的簡單封裝,因此自動釋放機制的核心就在於這個類。ui
AutoreleasePoolPage是一個C++實現的類spa
雙向鏈表
的形式組合而成(分別對應結構中的parent指針和child指針)id *next
指針做爲遊標指向棧頂最新add進來的autorelease對象的下一個位置因此,若當前線程中只有一個AutoreleasePoolPage對象,並記錄了不少autorelease對象地址時內存以下圖:線程
圖中的狀況,這一頁再加入一個autorelease對象就要滿了(也就是next指針立刻指向棧頂),這時就要執行上面說的操做,創建下一頁page對象,與這一頁鏈表鏈接完成後,新page的next
指針被初始化在棧底(begin的位置),而後繼續向棧頂添加新對象。指針
因此,向一個對象發送- autorelease
消息,就是將這個對象加入到當前AutoreleasePoolPage的棧頂next指針指向的位置code
每當進行一次objc_autoreleasePoolPush
調用時,runtime向當前的AutoreleasePoolPage中add進一個哨兵對象
,值爲0(也就是個nil),那麼這一個page就變成了下面的樣子:
objc_autoreleasePoolPush
的返回值正是這個哨兵對象的地址,被objc_autoreleasePoolPop(哨兵對象)
做爲入參,因而:
- release
消息,並向回移動next
指針到正確位置剛纔的objc_autoreleasePoolPop執行後,最終變成了下面的樣子:
知道了上面的原理,嵌套的AutoreleasePool就很是簡單了,pop的時候總會釋放到上次push的位置爲止,多層的pool就是多個哨兵對象而已,就像剝洋蔥同樣,每次一層,互不影響。
【附加內容】
值得一提的是,ARC下,runtime有一套對autorelease返回值的優化策略。
好比一個工廠方法:
+ (instancetype)createSark { return [self new]; } // caller Sark *sark = [Sark createSark];
秉着誰建立誰釋放的原則,返回值須要是一個autorelease對象才能配合調用方正確管理內存,因而乎編譯器改寫成了形以下面的代碼:
+ (instancetype)createSark { id tmp = [self new]; return objc_autoreleaseReturnValue(tmp); // 代替咱們調用autorelease } // caller id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替咱們調用retain Sark *sark = tmp; objc_storeStrong(&sark, nil); // 至關於代替咱們調用了release
一切看上去都很好,不過既然編譯器知道了這麼多信息,幹嗎還要勞煩autorelease這個開銷不小的機制呢?因而乎,runtime使用了一些黑魔法將這個問題解決了。
Thread Local Storage(TLS)線程局部存儲,目的很簡單,將一塊內存做爲某個線程專有的存儲,以key-value的形式進行讀寫,好比在非arm架構下,使用pthread提供的方法實現:
void* pthread_getspecific(pthread_key_t); int pthread_setspecific(pthread_key_t , const void *);
說它是黑魔法可能被懂pthread的笑話- -
在返回值身上調用objc_autoreleaseReturnValue
方法時,runtime將這個返回值object儲存在TLS中,而後直接返回這個object(不調用autorelease);同時,在外部接收這個返回值的objc_retainAutoreleasedReturnValue
裏,發現TLS中正好存了這個對象,那麼直接返回這個object(不調用retain)。
因而乎,調用方和被調方利用TLS作中轉,頗有默契的免去了對返回值的內存管理。
因而問題又來了,假如被調方和主調方只有一邊是ARC環境編譯的該咋辦?(好比咱們在ARC環境下用了非ARC編譯的第三方庫,或者反之)
只能動用更高級的黑魔法。
這個內建函數原型是char *__builtin_return_address(int level)
,做用是獲得函數的返回地址,參數表示層數,如__builtin_return_address(0)表示當前函數體返回地址,傳1是調用這個函數的外層函數的返回值地址,以此類推。
- (int)foo { NSLog(@"%p", __builtin_return_address(0)); // 根據這個地址能找到下面ret的地址 return 1; } // caller int ret = [sark foo];
看上去也沒啥厲害的,不過要知道,函數的返回值地址,也就對應着調用者結束此次調用的地址(或者相差某個固定的偏移量,根據編譯器決定)
也就是說,被調用的函數也有翻身作地主的機會了,能夠反過來對主調方乾點壞事。
回到上面的問題,若是一個函數返回前知道調用方是ARC仍是非ARC,就有機會對於不一樣狀況作不一樣的處理
經過上面的__builtin_return_address加某些偏移量,被調方能夠定位到主調方在返回值後面的彙編指令
:
// caller int ret = [sark foo]; // 內存中接下來的彙編指令(x86,我不懂彙編,瞎寫的) movq ??? ??? callq ???
而這些彙編指令在內存中的值是固定的,好比movq對應着0x48。
因而乎,就有了下面的這個函數,入參是調用方__builtin_return_address傳入值
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; }
它檢驗了主調方在返回值以後是否緊接着調用了objc_retainAutoreleasedReturnValue
,若是是,就知道了外部是ARC環境,反之就走沒被優化的老邏輯。
使用容器的block版本的枚舉器時,內部會自動添加一個AutoreleasePool:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { // 這裏被一個局部@autoreleasepool包圍着 }];
固然,在普通for循環和for in循環中沒有,因此,仍是新版的block版本枚舉器更加方便。for循環中遍歷產生大量autorelease變量時,就須要手加局部AutoreleasePool咯