最近,我搜尋了很多關於 iOS 記憶體管理的資訊,這是 Swift 中對我來說有點困難的題目。因此,我決定要更深入了解 Objective-C,因為我已經學過它,所以可以更透徹地了解記憶體管理,像是在 ARC 之前是如何進行記憶體管理的呢?有了 ARC 之後的情況又是如何?ARC 與垃圾回收器 (Garbage Collector) 有甚麼差異?諸如此類的問題。
簡介
當你開始撰寫物件 (Object) 時,會發現一件有趣的事。
物件比整數 (integer)、浮點 (float) 這類原始型別需要更多記憶體,無論這些物件是來自你自己建構的類別,或是創建自任何 Apple 框架的類別都是如此。
幸運的是,我們不必擔心要手動使用記憶體位置、及手動分配或解除分配記憶體區域(對某些語言而言)。而在 Objective-C 中,我們則使用參考計數 (Reference Counting)。
甚麼是參考計數?
當你的 App 啟動時,就會為你的物件提供一個記憶體區域。也就是說,你建立一個物件的同時,會在記憶體內要求一個區塊。現在,持有那個物件的變數就是指向該物件的指標,準確來說,是指向一個記憶體區域的指標。實際情況是,當物件被建立時,它會被授予一個稱作保留計數 (retain count) 或參考計數的東西,而這個東西表示特定物件的所有者數量。因此,我們可以想像它會是 1,如下圖所示:
這個名為 var 的變數,是一個記憶體區塊的指標。所以在程式碼中,你可以使用那個指標,並按需要呼叫方法。然而,當你到達程式碼區塊的最後時,這個變數將不在能被任何程式使用。這時,保留計數會回歸為 0,因為現在沒有任何東西指向這個記憶體區塊。而當運行引擎 (Runtime Engine) 說保留計數為 0 時,就沒有人在意這個記憶體區塊了,因此這個記憶體區塊將會被釋放,讓其他的物件使用。
可能會發生甚麼問題?
這裡唯一可能會出現問題的時間點,就是當你在建立物件,並將其從一處送到另一處時,所以我們不清楚指標是否仍在記憶體範圍內。直到 2011 年 ARC (Automatic Reference Counting) 功能出現之前,你都需要在用完物件時撰寫一些程式碼。
在 ARC 之前
在 ARC 功能加進 Objective-C 之前,我們必須做些這樣的事情:
MyClass *myObject = [[MyClass alloc] init];
[myObject myMethod]; // call methods
... // doing some stuff with the object
[myObject release]; // releasing the object
我們需要寫些程式碼來建立物件,物件建立了之後,就要呼叫物件的方法。但在某些時候,我們必須明確地呼叫 release
陳述句,這就是保留計數數字減少的原因。
如果物件數量少,這就不會是個問題。但是如果你擁有數百、甚至數千個物件被建立、操作、用為參數、或是在物件之間傳送,你就需要一直追蹤他們。如果想將一個物件從一個區域傳送到另一個區域,你可能無法確定是否可以釋放它、或是程式的其他部分是否會負責釋放,或甚至可能在你使用完之前就把它釋放了。因此,你也可以撰寫呼叫的內容,並保留對該物件的呼叫,但你仍需要將任何保留呼叫 (Retain Call) 與其他的釋放呼叫 (Release Call) 配對。
基本上來說,在 ARC 之前,你必須設想 App 可能經歷的每個情境邏輯 (scenario-logic),以確保所有物件的生命週期都被正確地管理,不過這並不容易。
有了 ARC 之後
幸運的是,有了 ARC 之後,你不再需要使用 release
、autorelease
、retain
這些呼叫。但重要的是,你要了解錯誤編寫這些程式碼可以造成的危險。
其中一個你可能遇到的問題,是你可能太快釋放。你會建立一個物件,然後有個指標指向記憶體區域,接著你會呼叫方法,最後在某個時間點釋放它。但是,如果你仍然持有一個有效的指標,那麼就沒有甚麼可以阻止你撰寫另一行程式碼來使用它。這名為迷途指標 (Dangling Pointer),指的是指標依舊存在,但它指向的記憶體區域不再有效,而這可能會導致閃退。
另外一個相反的問題,則是你可能沒有釋放,卻又建立了物件。開始呼叫物件的方法,然後讓指標脫離記憶體區域並消失,但你卻沒有釋放物件,這樣你就會需要越來越多記憶體,導致所謂的記憶體洩漏 (Memory Leak)。
所以,寫太多這樣的程式碼肯定容易出錯。現在你可能在想,明明我們正在使用這個新的 ARC 功能,為什麼我卻在說釋放以及保留呼叫的事呢。因為釋放及保留呼叫的概念沒有消失,這個程式語言仍會進行參考計數。
程式語言並沒有改變。有了 ARC 之後,不同之處只是你不再需要撰寫 retain、release、autorelease 呼叫語法,因為編譯器幫你寫了。ARC 認為編譯器的能力已經非常好,好到每當你編譯專案時,編譯器(這裡是 llvm,也就是 Xcode 在背後使用的東西)能夠確認程式碼的所有路徑。而且,它基本上遍歷你的程式碼,來整合所需的寫入、retain、release、autorelease 呼叫。
如果你真的非常擅於撰寫記憶體管理的程式碼,那麼編譯器所做的,就是更有效地寫出相同的程式碼。
ARC 實際上不會更改你的原始程式碼檔,但你在使用 ARC 編譯專案時,這就是編譯器做的事。
ARC 與垃圾回收器的差異
ARC 與垃圾回收器的效果截然不同。使用垃圾回收器的語言,通常是被稱為不確定性的 (Non-Deterministic) 語言。這意味著你無法準確地告知物件何時被回收,因為它是由一個外部程序在運行時管理的。而 ARC 則使我們是完全確定性的,程式碼會控制這些物件何時被釋放,因為釋放它們的程式碼是由編譯器撰寫,而不是你。事實上,不僅是你使用 ARC 時不用寫這些呼叫,而是你無法撰寫這些呼叫。如果你試著寫簡單的釋放呼叫,那麼將會碰到錯誤。
總結
你需要了解保留與釋放的概念,這些事情仍然在背後發生。不過當然,如果你從來沒有需要親手這樣做過,你應該感恩以後也沒有需要這樣做了。
希望你們可以留言分享你對這個題目的想法。你認為回到像 Objective-C 這樣的語言,是否可以更容易了解記憶體管理的題目?你有沒有試過這樣做?