首先這里先說一下RunLoop與自動釋放池的關(guān)系吧
我是按照網(wǎng)上總結(jié)的RunLoop與自動釋放池回答的面試官。當回答完這個以后,那么好接下來說一下自動釋放池是怎么釋放的釋放時機是什么,我當時回答是在autoreleasepool{}花括號結(jié)束釋放。這個回答不是面試官想要的答案, 我理解應(yīng)該 內(nèi)存管理引用計數(shù)器 的問題了吧?
autoreleased對象什么時候釋放
下面通過3個例子來看一下到底什么時候釋放的:
__weak NSString *string_A = nil;
__weak NSString *string_B = nil;
__weak NSString *string_C = nil;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *StrA = [NSString stringWithFormat:@"-----場景1--------"];
string_A = StrA;
@autoreleasepool{
NSString *StrB = [NSString stringWithFormat:@"-----場景2--------"];
string_B = StrB;
}
NSString *StrC = nil;
@autoreleasepool{
StrC = [NSString stringWithFormat:@"-----場景3--------"];
string_C = StrC;
}
NSLog(@"******viewDidLoad*******%@",string_A);
NSLog(@"******viewDidLoad*******%@",string_B);
NSLog(@"******viewDidLoad*******%@",string_C);
}
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
NSLog(@"******viewWillAppear*******%@",string_A);
NSLog(@"******viewWillAppear*******%@",string_B);
NSLog(@"******viewWillAppear*******%@",string_C);
}
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
NSLog(@"******viewDidAppear*******%@",string_A);
NSLog(@"******viewDidAppear*******%@",string_B);
NSLog(@"******viewDidAppear*******%@",string_C);
}
2018-12-24 10:50:01.614715+0800 Autoreleasepool[24851:3003734] ******viewDidLoad*******-----場景1--------
2018-12-24 10:50:01.614845+0800 Autoreleasepool[24851:3003734] ******viewDidLoad*******(null)
2018-12-24 10:50:01.614964+0800 Autoreleasepool[24851:3003734] ******viewDidLoad*******-----場景3--------
2018-12-24 10:50:01.615169+0800 Autoreleasepool[24851:3003734] ******viewWillAppear*******-----場景1--------
2018-12-24 10:50:01.615303+0800 Autoreleasepool[24851:3003734] ******viewWillAppear*******(null)
2018-12-24 10:50:01.615390+0800 Autoreleasepool[24851:3003734] ******viewWillAppear*******(null)
2018-12-24 10:50:01.618742+0800 Autoreleasepool[24851:3003734] ******viewDidAppear*******(null)
2018-12-24 10:50:01.618871+0800 Autoreleasepool[24851:3003734] ******viewDidAppear*******(null)
2018-12-24 10:50:01.619000+0800 Autoreleasepool[24851:3003734] ******viewDidAppear*******(null)
- 當使用 [NSString stringWithFormat:@"-----場景1--------"] 創(chuàng)建一個對象時,這個對象的引用計數(shù)為 1(看一下這篇文章就清楚為啥是1了) ,并且這個對象被系統(tǒng)自動添加到了當前的 autoreleasepool 中。當使用局部變量 string 指向這個對象時,這個對象的引用計數(shù) +1 ,變成了 2 。因為在 ARC 下 NSString *string 本質(zhì)上就是 __strong NSString *string 。所以在 viewDidLoad 方法返回前,這個對象是一直存在的,且引用計數(shù)為 2 。而當 viewDidLoad 方法返回時,局部變量 string 被回收,指向了 nil 。因此,其所指向?qū)ο蟮囊糜嫈?shù) -1 ,變成了 1 。
- 場景2
當通過 [NSString stringWithFormat:@"-----場景2--------"] 創(chuàng)建一個對象時,這個對象的引用計數(shù)為 1 。而當使用局部變量 string 指向這個對象時,這個對象的引用計數(shù) +1 ,變成了 2 。而出了當前作用域時,局部變量 string 變成了 nil ,所以其所指向?qū)ο蟮囊糜嫈?shù)變成 1 。另外,我們知道當出了 @autoreleasepool {} 的作用域時,當前 autoreleasepool 被 drain ,其中的 autoreleased 對象被 release 。所以這個對象的引用計數(shù)變成了 0 ,對象最終被釋放。 - 場景3
當出了 @autoreleasepool {} 的作用域時,其中的 autoreleased 對象被 release ,對象的引用計數(shù)變成 1 。當出了局部變量 string 的作用域,即 viewDidLoad 方法返回時,string 指向了 nil ,其所指向?qū)ο蟮囊糜嫈?shù)變成 0 ,對象最終被釋放。
在沒有手加Autorelease Pool的情況下,Autorelease對象是在當前的runloop迭代結(jié)束時釋放的,而它能夠釋放的原因是系統(tǒng)在每個runloop迭代中都加入了自動釋放池Push和Pop
Autorelease原理
AutoreleasePoolPage
ARC下,我們使用@autoreleasepool{}來使用一個AutoreleasePool,隨后編譯器將其改寫成下面的樣子:
void *context = objc_autoreleasePoolPush();
// {}中的代碼
objc_autoreleasePoolPop(context);
而這兩個函數(shù)都是對AutoreleasePoolPage的簡單封裝,所以自動釋放機制的核心就在于這個類。

- 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í)行后,最終變成了下面的樣子:
