重拾 ObjC 自動釋放池

Smallfly發表於2019-03-17

Objc 自動釋放池平時很少顯式的使用,但其實它時刻在默默為我們工作。關於自動釋放池原始碼分析的文章已經很多了,本文不會在原始碼層面剖析原理。

初衷

在 MRC 時代,需要使用retainrelease手動維護物件的引用計數,並要遵循「誰建立誰釋放」的原則。

然而在某些場景下無法滿足這個原則,比如說工廠方法:

+ (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,就會被立即釋放。

  1. 普通區域性物件 區域性物件在超出作用域並且引用計數為 0 時會立即釋放。
override func viewDidLoad() {
	super.viewDidLoad()
	let o = NSObject()
}
// `o` has release
複製程式碼
  1. autorelease 物件
override func viewDidLoad() {
	super.viewDidLoad()
	let imgO = UIImage(named: “image”);
}
// imgO 還未釋放,會等到 autoreleasepool pop 才釋放。
// 可使用 weak 指標測試,在 viewWillApper 方法該物件仍然存在
// 或使用符號斷點,檢測 AutoreleasePoolPage::autorelease() 方法被呼叫
複製程式碼

相關文章