文章也同時在個人博客 http://kimihe.com/更新
引言
OC對象的生命周期取決于引用計數(shù),我們有兩種方式可以釋放對象:一種是直接調(diào)用release釋放;另一種是調(diào)用autorelease將對象加入自動釋放池中。自動釋放池用于存放那些需要在稍后某個時刻釋放的對象。
本文將介紹自動釋放池的原理和使用場景,并結(jié)合一道據(jù)說是優(yōu)酷iOS的筆試題來舉例說明自動釋放池的妙用。
更多關(guān)于iOS內(nèi)存管理的文章已經(jīng)匯總至:深入總結(jié)iOS內(nèi)存管理。
自動釋放池的創(chuàng)建
如果沒有自動釋放池而給對象發(fā)送autorelease消息,將會收到控制臺報錯。但一般我們無需擔(dān)心自動釋放池的創(chuàng)建問題。
我們的Mac以及iOS系統(tǒng)會自動創(chuàng)建一些線程,例如主線程和GCD中的線程,都默認(rèn)擁有自動釋放池。每次執(zhí)行 “事件循環(huán)”(event loop)時,就會將其清空,這一點(diǎn)非常重要,請務(wù)必牢記! 關(guān)于事件循環(huán),其涉及到runloop,可以看這篇文章:深入理解RunLoop。
因此我們一般不需要手動創(chuàng)建自動釋放池,通常只有一個地方需要它,那就是在main()函數(shù)里,如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
這個main()函數(shù)里面的池并非必需。因為塊的末尾是應(yīng)用程序的終止處,即便沒有這個自動釋放池,也會由操作系統(tǒng)來釋放。但是這些由UIApplicationMain函數(shù)所自動釋放的對象就沒有池可以容納了,系統(tǒng)會發(fā)出警告。因此,這里的池可以理解成最外圍捕捉全部自動釋放對象所用的池。
@autoreleasepool的作用
大家可以先看一下下面的iOS筆試題的第5題(修改代碼的錯誤),如下圖:

這段代碼問題在哪里呢?題目的解答請繼續(xù)閱讀。筆者先給一個提示:與內(nèi)存的釋放有關(guān)。
現(xiàn)考慮如下代碼:
for (int i = 0; i < 10000; i++) {
[self doSthWith:object];
}
這段代碼和筆試題關(guān)鍵部分大同小異。如果"doSthWith:"方法要創(chuàng)建一個臨時對象,那么這個對象很可能會放在自動釋放池里。筆試題中最后stringByAppendingString方法很有可能屬于上述的方法。因此如果涉及到了自動釋放池,那么問題也應(yīng)該就出在上面。
注意:即便臨時對象在調(diào)用完方法后就不再使用了,它們也依然處于存活狀態(tài),因為目前它們都在自動釋放池里,等待系統(tǒng)稍后進(jìn)行回收。但自動釋放池卻要等到該線程執(zhí)行下一次事件循環(huán)時才會清空,這就意味著在執(zhí)行for循環(huán)時,會有持續(xù)不斷的新的臨時對象被創(chuàng)建出來,并加入自動釋放池。要等到結(jié)束for循環(huán)才會釋放。在for循環(huán)中內(nèi)存用量會持續(xù)上漲,而等到結(jié)束循環(huán)后,內(nèi)存用量又會突然下降。
而如果把循環(huán)內(nèi)的代碼包裹在“自動釋放池”中,那么在循環(huán)中自動釋放的對象就會放在這個池,而不是在線程的主池里面。如下:
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *str = @"abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
}
}
新增的自動釋放池可以減少內(nèi)存用量,因為系統(tǒng)會在塊的末尾把這些對象回收掉。而上述這些臨時對象,正在回收之列。
自動釋放池的機(jī)制就像“棧”。系統(tǒng)創(chuàng)建好池之后,將其壓入棧中,而清空自動釋放池相當(dāng)于將池從棧中彈出。在對象上執(zhí)行自動釋放操作,就等于將其放入位于棧頂?shù)哪莻€池。
實(shí)驗驗證
我們可以通過實(shí)驗進(jìn)行驗證。新建工程加入上述代碼,并關(guān)閉ARC(不然是看不到區(qū)別的)。
在未添加autoreleasepool時,我們的堆內(nèi)存實(shí)時分配情況如下圖:

大家可以看到Persistent Bytes不斷增加,到達(dá)100W次的創(chuàng)建峰值后(出for循環(huán))開始逐步釋放。因此圖像是一個向上凸的曲線。
而在加入autoreleasepool后,我們看到如下的曲線:

可以發(fā)現(xiàn)盡管字符串在不斷地創(chuàng)建,但由于得到了及時的釋放,堆內(nèi)存始終保持在一個很低的水平。
其他注意點(diǎn)
@autoreleasepool語法還有一個好處,就是可以避免無意間誤用那些在清空池之后已被系統(tǒng)回收的對象,例如:
@autoreleasepool {
id obj = [self createObject];
}
[self useObject:obj];
上述代碼在編譯時就會基于錯誤警告,因為obj出了自動釋放池就不可用了。
@autoreleasepool小結(jié)
- 自動釋放池排布在棧中,對象受到autorelease消息后,系統(tǒng)將其放入棧頂?shù)某乩铩?/li>
- 合理運(yùn)用自動釋放池,可以降低程序的內(nèi)存峰值。