範圍
主題33:使用弱式參考避免保留循環
主題34:利用自動釋放緩衝池區塊來減少高記憶體耗用水位
主題30:使用 ARC 讓參考計數更加容易處理
整理
主題33
承繼主題01及其他記憶體相關的主題,我們已經知道:物件的存在與否在於它是否被其他物件所擁有。筆者個人喜歡用「需要」來形容。在語言的設計方面,Objective-C 採用計數的方式。假設有物件A需要使用到物件B,就程式碼來看,物件B會透過物件A所擁有的建構子、靜態工廠方法或 setter method 被注入到物件A,那麼對物件B就會在自己的計數器上加1,而只要物件的計數器不小於等於0,則代表該物件有存在的價值,而 Runtime library 就不會對該物件的記憶體位址進行「標註可利用/標註原物件無效」。
上面的解釋大抵上是對的,但是其實還有一個條件要滿足。
Objective-C 支援 multi-threads,其中有一個和應用程式生命週期相等的 thread :main thread。(並不代表所有和應用程式生命週期一樣的都是 main thread,main thread 是一定有而且唯一的)
任何與 main thread 掛勾不到的物件,也就是說,以樹狀觀念來看,main thread 若是主幹,我們先把其他 thread 視為分支。試想,若有二個物件,它們擁有/需要彼此,因為其計數值都大於0,但是它們卻不在任何 main thread 可觸及到的任何分支上,那是什麼樣的情形。
簡單講,那代表應該程式已經喪失該物件的操作能力,而且即使它們永遠不會被應用程式所利用,也不會因此而消失:因為它們需要彼此。它們(至少需要有二個以上的物件才會形成這個結構,重提主題01:就是孤島物件(群)。
要對付這樣的孤島物件群,有個直覺的作法:引入垃圾收集機制(Garbage Collection, GC)。這項技術是 Java 以往被強調的長項。不過 Mac OS X 10.8 已宣佈不在維護這個環境,而 iOS 本來就沒有支援,又是為什麼呢?
道理很簡單,GC機制本身是一個 daemon thread,它本身也會長期佔用資源。試想,當執行環境因突發狀況的發生,而導致連這個 daemon thread 都要不到記憶體來執行時,GC 就會變得完全發揮不了功效,甚至成為系統「卡還會再卡」的幫兇。
對於原本記憶體環境就相對不優沃的行動裝置而言,iOS 不支援 GC 是項明智的選擇。而 Mac OS X 進一步地捨棄這個機制,就代表 Apple 認為總有更好的作法:那就是將在主題30進行討論的 ARC 機制。
回到孤島群的解決方法:利用在 property attribute 或一般/區域變數宣告時,加入「unsafe_unretained」或「weak」的宣告即可。
以上面提及物件A及物件B的例子來說明:假設物件A需要物件B,物件B對物件A的關係是 unsafe_unretained 或 weak 的話,代表:物件B並不擁有物件A,換個語意說來有點悲傷,今天當物件A計數歸零時,物件B也不會強求,覺得物件A消失就消失吧。一旦物件A消失,對於物件B來說,也不過就是需要它的物件少了一個。若物件B因為物件A的消失而同時也不再為任何物件需要,那麼物件B也就得消失。
unsafe_unretained 和 weak 的不同是:假設物件A的計數歸零,物件B原此指向物件A的 reference 會不會設定成 nil ? weak 會那麼做。由 Objective-C 有著:對 nil 發送訊息也不會出錯的特性,所以使用 weak 相對地比 unsafe_unretained 安全。(unsafe_unretained 自己都這麼命名了) 那麼,進一步的疑問題是:那為何需要 unsafe_unretained ?都使用 weak 不是比較安全?答案是:這是給程式設計者更靈活的選擇。有時,我們希望物件A的計數歸零時,我們的程式會得到感知,這個感知因應著一項重要邏輯,而這個邏輯不成立時,應不惜癱瘓讓整個應用程式。
最後加註個名詞解釋:unsafe_unretained 與 weak 是由 Runtime 中被實作,其中 weak 提供的功能叫「Autonilling」。
主題34
大家都有存發票的經驗嗎?從店家取得發票後,它會被放在你的皮夾、皮包或口袋裡,然後可能被放置在桌上,總之最後可能找個袋子或盒子裝起來。然後等到對獎的時間到了(最長二個月),終於可以決定那一張張小片小片的紙到底是黃金?還是垃圾?(講到這個,筆者的人生經驗一向很沒故事可以講…就是不會中的那種…)
發票有這種特性:不到開獎的號碼列出來之前,我們沒辦法判定它是否還需要被保留。有些物件也有類似的特性:不到某個周期結束之前,我們沒辦法決定是否還需要保留它。
從 Objective-C 的記憶體管理方式我們知道了有保存該物件的方式:發送 retain 訊息;或是反過來發送 release 訊息,明確我們對某物件的需求中止。但是,試想:如果我們在某方法內創建了某物件,而該物件做為該方法的回傳值,我們不就無法在該方法的執行範圍內自行決定該回傳值是否該被 release 了嗎?這種時候,就無法做到「自己創的物件自己要負責放」的原則,難道只能經由文件或其他可能的方式將物件釋放的任務轉稼給持有該回傳值的外部物件嗎?
答案是否定的。「自己創的物件要自己負責放」的原則不可以違反,但是釋放的時機點可以被延遲!這個就是 autorelease pool 的概念原型。
我們可以建一個 autorelease pool,語法如下:
@autoreleasepool {
//範圍A
}
在「範圍A」中任何被傳遞 autorelease 的物件,都會在脫離自己的生命週期範圍後,被佔時地丟到 autoreleasepool 中,直到「範圍A」的執行周期結束,Runtime 會對被放在 autoreleasepool 中的物件進行檢核及清除。就像發票在二個月的開獎時間到之後,才知道它們該被丟掉還是保留。
autoreleasepool 在 main.m 中可以看到系統已經預設加上了一個,儘管因為它和 main function 的生命週期一致,所以在技術上是不必要的,但是它可以讓有使用 autorelease 訊息的程式碼不會在 console 上出現「xx autoreleased with no pool in place - just leaking -..」的訊息。所以這個預設加上的 autoreleasepool 被稱為「catch-all pool」。
autoreleasepool 可以巢狀。雖然,在足夠好的設計下,我們可能很少有機會用到。但是假設我們有個需要在記憶體中暫時積存很多物件物件的演算法,我們就可以用這種套疊式的 autoreleasepool 來限縮 pool 的複檢範圍、減短物件被回收所需等待的時間。這在技術上是可以達到,但是應該盡量避免的,因為它至少會讓程式碼的意圖被分散:商業邏輯和最佳化語法的混雜,會使程式碼的可閱讀性大幅下降。
取而代之的,就是盡可能讓商業邏輯有明確的限制、讓記憶體的佔用空間事件化、可重複利用他。若真的不得不使用較複雜的記憶體管理邏輯,那就使用物件導向的最基本原則吧:封裝它!可能模式被導入,或是機制被導入。總之,盡可能在高層次隱藏和商業邏輯無關的程式碼,讓原來的邏輯意圖更明確,也讓複雜的管控機。
主題30
是什麼新的記憶體管理機制奠基於手動記數(MRC),甚至最後讓 Apple 決定在 Mac OS X 中明定終止對 GC 機制的維護的呢?答案就是 ARC (Automatic Reference Counting)。
ARC 的出現,是意圖讓程式員可以別把心力放在 retain、release 的精細微調上,而更能極中精神於「你要做的是什麼」,而非「你要如何做」。
ARC 機制同時實作在「編譯期」及「執行期」。編譯期方面,以「靜態分析器」找出原程式碼參數計數上需要補齊的地方,然後在編譯後幫程式員加上原本該自己加上的「retain、release、autorelease」。所以程式員就不用自己寫了;更正確地說:程式員也不能自己寫了,不然會造成靜態分析器的困擾。
此外,以往 MRC 的寫作定律中有一條:應該在 dealloc method 中將使用到的變數參考進行釋放及設值為 nil 的規定,也一併被靜態分析器做掉了,所以程式員也不該呼叫、實作 dealloc method。
ARC 在執行期能更進一步地替靜態分析器無法做到的最佳化做出更好的處理:不但必需與非 ARC 的程式碼兼容,又能協助靜態分析器難以最佳化的自動釋放機制。Runtime library 提供了一些特殊函式,例如:objc_autoreleaseReturnValue 、objc_retainAutoreleasedReturnValue。
ARC 對於 property attribute 的支援上,在之前的主題已經提過的,包括:strong, weak, unsafe_unretained,而 ARC 進一步地對區域變數及實體變數也提供了:__strong、__weak、__unsafe_unretained及 __autoreleasing 這幾個限定詞,置於型別與 reference 名稱中間。(均以兩個底線做為前置符號)
其中, __weak 經常用於某個在 block 中會使用到、但是宣告處外於該 block 的變數。這是為了打破 block 引進的保留循環:區塊會自動保留它所獲得的全部物件。在主題 40會討論如何避免 block 引進的保留循環,但是並沒有仔細說明為什麼區塊會自動保留物件。
不過若仔細考量 block 的行為,就可以留意到其非線性執行時機的特性,的確是需要有個保留機制,讓它能操作約定好要使用的物件。一般書上會說:block 內無法改變宣告在該 block 外的變數,這個說法雖然在表達行為結果上沒有錯,不過實作上的內容不如說 block 內取得的物件往往是複本要來得正確。至少有不少語言是這樣實作的。
ARC 使用了特定的方法命規則:若方法名稱以「alloc, new, copy, mutableCopy」為首,則方法的呼叫者就「擁有」該方法回傳的物件。這個命名規則會讓不知道此規則的人在使用 retainCount method 帶來困擾。由於 ARC 會自動維護這些規則,所以其實 API 的使用者並不太需要進一步的了解;不過若為 API 的開發者,則要多留意方法的命名,並始終了解內部動作的實際執行方法如何比較好。