在〈閉包〉一文中,我們介紹了如何透過 [weak self] 打破循環參照,但還留下了一個問題:除了 weak 之外還有 unowned,它們有什麼不同,又該在什麼時候使用?要回答這個問題,必須先深入了解 ARC 實際如何運作。
這是 Swift 中級系列第 1 篇。我們會整理 ARC 的運作原理、strong、weak、unowned 三種參照的確切差異,以及實務上選擇「weak vs unowned」的判斷準則。從 Objective-C 時代的 MRC(Manual Reference Counting,手動參照計數)轉移到 ARC 的歷史已在其他文章介紹,本文聚焦於 Swift 觀點下的現況。
ARC 的本質 — 不是執行階段清潔工,而是編譯器整理帳本
很多人把 ARC(Automatic Reference Counting)當成「Swift 的垃圾回收器」,但兩者的運作方式有根本差異。
垃圾回收(GC)會由獨立的執行階段系統定期執行,找出「沒有人使用的物件」並清理。相較之下,ARC 在編譯時就決定了。編譯器會分析程式碼流程,在建立參照的位置插入 retain(計數 +1),在參照結束的位置插入 release(計數 -1)。計數變成 0 的瞬間會呼叫 deinit,並釋放記憶體。
這項差異帶來兩個重要特性。第一,釋放時機是確定性的(deterministic)。不是像 GC 那樣「之後總會清理」,而是在最後一個參照消失的那一行立即釋放。這就是可以把資源清理交給 deinit 的依據。第二,沒有會暫停程式的清理階段。這正是哲學系列第 1 篇所說「不為了安全犧牲效能」的實例。
它並非沒有成本。參照計數的增減必須具備執行緒安全,因此會使用原子操作,這就是參考型別隱藏的成本。這也是 Swift 預設偏好 struct 的原因之一,因為值型別不需要整理這本帳。更關鍵的是,ARC 無法解決循環。GC 能找出「從根節點無法抵達的物件群組」,即使它們形成循環也能清理;但 ARC 中,只要物件彼此互相計數,計數就不可能變成 0。這就是循環參照必須由程式設計師負責的結構性原因。
三種參照 — 所有權的語言
在 ARC 的世界裡,參照會依照「我是否有責任讓這個物件存活」分成三種。
strong(預設值)代表擁有。 它會增加計數,只要我持有它,物件就會存活。所有未加標記宣告的參照都是 strong。
weak 是不擁有物件的觀察。 它不會增加計數,因此其餘 strong 參照全部消失時,物件就會被釋放。此時執行階段會自動將 weak 參照改成 nil。因此 weak 變數必須是選擇型別,而且必須宣告為 var。「隨時可能變成 nil」這個事實,就像選擇型別一文所說,會被刻在型別裡。
unowned 同樣是不擁有物件的參照,但它放棄了 nil 處理。 它和 weak 一樣不增加計數,但在目標釋放後存取它時,不會得到 nil,而是立即當機。相對地,它不是選擇型別,因此不需要解包即可使用。
總結來說:strong 是「我會讓你存活」,weak 是「我知道你可能消失」,unowned 是「我確信你會比我活得久」。
weak vs unowned — 選擇標準是生命週期關係
那麼,什麼時候該用 weak,什麼時候該用 unowned?判斷標準不是語法差異,而是兩個物件之間的生命週期關係。
如果對方可能比你更早消失,就使用 weak。 委任者是教科書等級的例子。當檢視畫面參照委任者(通常是檢視控制器)時,檢視控制器先被釋放是完全正常的情境。因此委任者屬性依慣例會宣告為 weak var delegate,使用時則透過選擇鏈(delegate?.didFinish())表達「不存在就忽略」。在這種關係中,nil 是正常狀態。
如果結構上保證對方與你同時或比你更晚消失,就使用 unowned。 教科書案例是信用卡與客戶。卡片無法脫離客戶存在,而卡片存活期間客戶一定存活。unowned let customer就是精確的表達。它不是選擇型別,因此不必每次使用都解包,也能用 let 宣告以維持不可變性。
把這個標準與強制解包 ! 的標準重疊比較,就能看出一致性。unowned 是參照版本的 !。它宣告「如果這裡是 nil,就代表我的設計已經失效」,而當這個確信錯誤時,會立即以當機告知你,而不是靜默地錯誤運作。沒有把握時就使用 weak。實務上的主流原則確實是「不確定就用 weak」。weak 的成本(選擇型別處理與些微執行階段額外負擔)比當機風險更低。把 unowned 視為只在程式碼結構清楚呈現生命週期保證的地方謹慎使用的工具,才是安全做法。
閉包擷取也適用相同標準。[weak self]之所以是預設選擇,是因為大多數情況下,無法保證閉包執行時 self 仍然存活。反之,若閉包與 self 的生命週期綁定,例如在 lazy 屬性的立即執行閉包中擷取 self,使用 unowned 就有正當性。屬性存活就代表 self 存活。
用工具確認 — 靠量測,不靠感覺
循環參照無法只靠程式碼審查全部抓出來。記住三種確認工具,就能把「應該沒問題吧」變成「已經確認」。
deinit 紀錄是最便宜的工具。 關閉畫面後,如果沒有看到檢視模型的 deinit 紀錄,就表示某處有洩漏。開發時養成在可疑類別加上一行 print 的習慣,就能提早發現大多數循環。
Xcode 的 Memory Graph Debugger 會用圖形呈現循環。 執行時按下除錯列的記憶體圖形按鈕,就能以圖形查看目前堆積區的物件與參照關係。如果本應釋放的物件仍然存在,可以沿著箭頭追查是誰持有它。疑似洩漏的物件也會以紫色驚嘆號標示,這同樣是線索。
Instruments 的 Leaks 範本是沿時間軸進行量測。 反覆開啟與關閉畫面,觀察記憶體是否呈階梯狀成長,因此適合在發佈前定期檢查。
順帶一提,循環參照的常見嫌疑犯很固定:儲存在屬性中的閉包(閉包篇)、誤將委任者宣告為 strong,以及忘記解除 NotificationCenter 或計時器類型的註冊(NSTimer 篇)。審查新程式碼時,只要查看這三個位置,就能涵蓋大多數循環。
總結
- ARC 不是執行階段清潔工,而是在編譯時插入 retain/release 的機制。釋放時機是確定性的,也沒有 GC 暫停,但 ARC 無法自行解決循環。
- 參照是所有權的語言。strong 是擁有,weak 是知道可能為 nil 的不擁有,unowned 是確信對方會活得更久的不擁有。
- 選擇標準是生命週期關係。對方可能先消失就用 weak,結構上保證對方活得更久就用 unowned,不確定就用 weak。
- unowned 是參照版本的強制解包。只在程式碼中能讀出這份確信的地方使用。
- 透過 deinit 紀錄、Memory Graph Debugger、Instruments Leaks 進行量測確認,而不是憑感覺。
下一篇是中級系列第 2 篇:泛型。我們會整理尖括號中的 T 如何同時實現型別安全與程式碼重用,以及 where 子句何時是必要的。

![[Swift 中級 #1] Swift ARC 完整解析:用生命週期關係選擇 weak vs unowned 封面圖](/assets/images/posts/dea74de0-7d82-4c90-b8e1-e7be2bdaf005/1.jpg)