Swift 與 Objective-C

Objective-C 記憶體管理歷史:從 MRC 到 ARC 完整總結

進行 iOS 開發時,總有一件事會讓人好奇。

閱讀 6 分鐘
Objective-C 記憶體管理歷史:從 MRC 到 ARC 完整總結 封面圖

進行 iOS 開發時,總有一件事會讓人好奇。

「為什麼以前的 Objective-C 程式碼裡,會寫滿 retainrelease?」

如果你是從 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)時代,這個數字必須由人手動管理。

寫滿 retain 和 release 的舊程式碼,第一次看到時確實會讓人不知所措
寫滿 retain 和 release 的舊程式碼,第一次看到時確實會讓人不知所措

規則很明確,通常會用「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)」。

開發人員不必再寫 retainrelease,看起來似乎方便多了。

但結果並不理想。

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 也不是萬能的。

由於它無法自動解開彼此抓住對方的「循環參照」,所以開發人員仍須使用 weakunowned 這類弱參照來切斷它。

這個概念也原封不動地延續到我們現在使用的 Swift。


常見問題(Q&A)

Q. 現在還需要學習 MRC 嗎?

實務上幾乎不會再從頭使用 MRC 撰寫程式碼。不過,老舊的函式庫或面試中可能會被問到相關概念,因此了解基本原理仍然很有幫助。

Q. autorelease 在 ARC 中也會使用嗎?

雖然不需要直接呼叫,但這個機制本身在 ARC 底層仍然持續運作。因此 @autoreleasepool 區塊現在仍是有效的工具。

Q. Swift 也使用 ARC 嗎?

是的,Swift 也是以 ARC 為基礎。因此循環參照與 weak 概念在 Swift 中同樣重要。


簡單總結,Objective-C 記憶體管理是沿著「手動計數的 MRC → 短暫的 GC 實驗 → 由編譯器計數的 ARC」一路發展而來。

如今理所當然使用的自動記憶體管理,其實是建立在無數次崩潰與試錯之上,逐步打磨而成的結果。

希望這段演進脈絡能成為正在學習 iOS 開發的你的一張小地圖。祝你今天也享受寫程式的樂趣!


參考資料

延伸閱讀