Objc 自動釋放池平時很少顯式的使用,但其實它時刻在默默為我們工作。關於自動釋放池原始碼分析的文章已經很多了,本文不會在原始碼層面剖析原理。
初衷
在 MRC 時代,需要使用retain
和release
手動維護物件的引用計數,並要遵循「誰建立誰釋放」的原則。
然而在某些場景下無法滿足這個原則,比如說工廠方法:
+ (id)factory {
return [self new];
}
複製程式碼
在return
處如果呼叫retain
,就需要呼叫方負責 release
,這顯然是不科學的設計。所以這裡不能retain
, 但是不 retain
,該物件超出作用域後就會被釋放,呼叫方取到的會是 nil
。該如何保證呼叫方在這個物件超出作用域後,還能取到呢?方法就是自動釋放池,在返回前,將該物件被加入自動釋放池,這樣呼叫方就能順利取到返回值了。
那如果物件真的需要被釋放了,如何從自動釋放池裡移除?熟悉 RunLoop 的同學應該知道,在 RunLoop 喚醒和即將睡眠狀態之間會被插入自動釋放池,每次 RunLoop 迭代都會向本次迭代加入的物件傳送一條release
訊息。如果物件的引用計數變為 0,便會被釋放。
實現和實踐
自動釋放池雖然被叫做”池“,其實它是一個棧結構。棧的實現方式有很多,自動釋放池採用了雙向連結串列,能夠比較方便的實現 push 和 pop。
自動釋放池之所有用棧實現,而不用其他資料結構,比如說雜湊表?是因為 autorelease 物件往往需要批量處理,比如說一次 RunLoop 迭代生成一大批的 autorelease 物件。
所以這裡就出現一個問題,假如某一段程式碼在一次 RunLoop 週期內生成大量的 autorelease 物件,還沒有等到迭代結束清理,就已經記憶體溢位了,該怎麼辦?
這是就需要手動的來觸發 autorelease 物件的釋放。@autoreleasepool{}
登場,它能夠控制 autorelease 物件釋放的顆粒度。
{
for (NSInteger i = 0; i < 10000; i++) {
@autoreleasepool {
NSImage *img = [NSImage imageNamed:@"aimge"];
}
}
}
複製程式碼
上述的極端例子,如果for
迴圈中不巢狀 autoreleasepool,在 Xcode 側邊欄的 Debug Session,能看到應用的佔用的記憶體不斷的增加。
在巢狀使用 autoreleasepool 的場景,並且需要由內而外逐層清理,所以使用棧最適合不過了。
釘子和錘子
以前面試幾乎每次都會被問物件是什麼時候釋放的,一般的回答是引用計數為 0,但這只是站在物件的角度考慮的,那什麼時候物件的引用計數會變為 0 呢?
因為了解過自動釋放池,所以會說是在 autoreleasepool pop 的時候,如果沒有手動的新增 autoreleasepool 便會在 RunLoop 迭代的時候引用計數被減為 0 時釋放。
其實回答的有些片面,並不是所有物件都是在 pop 的時候引用計數才會為 0 的,普通區域性物件其實在超出作用域時(大括號)引用計數為 0,就會被立即釋放。
- 普通區域性物件 區域性物件在超出作用域並且引用計數為 0 時會立即釋放。
override func viewDidLoad() {
super.viewDidLoad()
let o = NSObject()
}
// `o` has release
複製程式碼
- autorelease 物件
override func viewDidLoad() {
super.viewDidLoad()
let imgO = UIImage(named: “image”);
}
// imgO 還未釋放,會等到 autoreleasepool pop 才釋放。
// 可使用 weak 指標測試,在 viewWillApper 方法該物件仍然存在
// 或使用符號斷點,檢測 AutoreleasePoolPage::autorelease() 方法被呼叫
複製程式碼