進行 iOS 開發時,總有一件事會讓人好奇。
「為什麼以前的 Objective-C 程式碼裡,會寫滿 retain、release?」
如果你是從 Swift 入門,第一次打開舊程式碼時,應該多少會被這種景象嚇到。
今天就來整理 Objective-C 記憶體管理的歷史,也就是它如何從 MRC 發展到 ARC。
先說結論,重點如下。
Objective-C 的記憶體管理,從「開發人員親自計數的時代(MRC)」走向「由編譯器代為計數的時代(ARC)」。
中間雖然曾短暫實驗過垃圾收集,但最終 ARC 在 2011 年登場,確立了整體方向。
接下來會依照不同時代逐一說明。
什麼是參照計數?記憶體管理的基本概念
在進入正式的歷史之前,先掌握一個概念就好。
那就是「參照計數(reference count)」。
可以把它想成每個物件都有一個數字,用來計算「目前有幾個人抓住我」。
只要這個數字大於等於 1,物件就還活著;變成 0 時,就會從記憶體中釋放。
因此,抓住物件時就增加數字(retain),放開物件時就減少數字(release)。
這個簡單的規則,就是 Objective-C 記憶體管理的根基。
MRC 時代:開發人員親自計數的年代
在 ARC 之前,也就是 MRC(Manual Retain Count)時代,這個數字必須由人手動管理。
規則很明確,通常會用「NARC」這個詞來記憶。
- 由 New、Alloc、Retain、Copy 建立的物件,由我負責
- 負責的物件使用完後,必須執行
release
實際的程式碼會長這樣。
// 建立物件時,參照計數會 1
NSObject *obj = [[NSObject alloc] init];
[obj retain]; // 計數 2
[obj release]; // 計數 1
[obj release]; // 計數 0 → 釋放記憶體
問題就出在這裡。
如果忘了 release,記憶體就會洩漏;太早釋放則會碰到已經消失的物件,導致 App 當掉。
這就是所謂的「殭屍物件」崩潰。
因為必須一直在腦中計算目前抓住的物件有幾個參照,實在是非常費心。
autorelease:「不是現在,晚一點再放開你」
MRC 時代還有另一種棘手的情況。
那就是方法建立新物件並將它傳回外部時。
- (NSString *)greeting {
NSString *msg = [[NSString alloc] initWithString:@"你好"];
return msg; // 我 alloc 建立了,所以 release 要做;但什麼時候?
}
按照規則,呼叫 alloc 的一方必須執行 release。
但如果在 return 之前就 release,接收方還沒開始使用物件,它就消失了。可是完全不做又會造成洩漏。
為了解決這個兩難,autorelease便應運而生。
也就是預約「不要現在立刻 release,晚一點再 release」。
套用 autorelease 的物件會加入名為「自動釋放池(autorelease pool)」的等待清單,等到釋放池清空時(通常是執行迴圈跑完一輪時)一次接受 release。
因此,接收方就能安全地取得並使用物件。
像 [NSString stringWithFormat:] 這類不需 alloc、直接取得並使用的便利建構方法,回傳的物件全都採用了這種方式。
順帶一提,這個自動釋放池在 ARC 時代也以 @autoreleasepool 區塊的名稱存留下來。它是在迴圈中記憶體暴增時可以派上用場的實戰工具,內容較多,因此另外整理成一篇文章。
垃圾收集為什麼會失敗?
Apple 也知道這種不便。
因此在 2007 年的 Mac OS X 10.5 Leopard 中,導入了像 Java 一樣自動清理記憶體的「垃圾收集(GC)」。
開發人員不必再寫 retain、release,看起來似乎方便多了。
但結果並不理想。
GC 在背景執行時,可能會讓 App 在難以預測的時機短暫卡頓。更重要的是,對重視效能與電池續航力的 iPhone 來說,負擔相當大。
因此它始終沒有導入 iOS。
最後,Mac 版 GC 也從 2012 年的 OS X 10.8 起逐步停止使用(deprecated)。
ARC 登場:由編譯器代為計數
接著在 2011 年,Apple 提出的解法就是 ARC(Automatic Reference Counting)。
它隨著 Xcode 4.2、iOS 5 以及 OS X 10.7 Lion 一同公開。
ARC 的構想相當巧妙。
保留參照計數的方式,但讓編譯器而非開發人員,自動插入
retain·release。
它不像 GC 一樣在執行階段另外運作清理程式,而是在編譯時期,將釋放程式碼自動插入恰到好處的位置。
因此幾乎不會增加執行階段的效能負擔,同時也能將記憶體管理自動化。
將這兩個時代整理成表格,大致如下。
| 分類 | MRC | ARC |
|---|---|---|
| 登場時間 | 初期~2011 年 | 2011 年(iOS 5) |
| retain/release | 手動撰寫 | 由編譯器自動插入 |
| autorelease | 手動呼叫 | 由編譯器管理 |
| 記憶體洩漏風險 | 高 | 大幅降低 |
| 效能負擔 | 無 | 幾乎沒有 |
不過,ARC 也不是萬能的。
由於它無法自動解開彼此抓住對方的「循環參照」,所以開發人員仍須使用 weak 或 unowned 這類弱參照來切斷它。
這個概念也原封不動地延續到我們現在使用的 Swift。
常見問題(Q&A)
Q. 現在還需要學習 MRC 嗎?
實務上幾乎不會再從頭使用 MRC 撰寫程式碼。不過,老舊的函式庫或面試中可能會被問到相關概念,因此了解基本原理仍然很有幫助。
Q. autorelease 在 ARC 中也會使用嗎?
雖然不需要直接呼叫,但這個機制本身在 ARC 底層仍然持續運作。因此 @autoreleasepool 區塊現在仍是有效的工具。
Q. Swift 也使用 ARC 嗎?
是的,Swift 也是以 ARC 為基礎。因此循環參照與 weak 概念在 Swift 中同樣重要。
簡單總結,Objective-C 記憶體管理是沿著「手動計數的 MRC → 短暫的 GC 實驗 → 由編譯器計數的 ARC」一路發展而來。
如今理所當然使用的自動記憶體管理,其實是建立在無數次崩潰與試錯之上,逐步打磨而成的結果。
希望這段演進脈絡能成為正在學習 iOS 開發的你的一張小地圖。祝你今天也享受寫程式的樂趣!

