最近新接手了一個舊的 iOS 專案,在熟悉程式碼的過程中,我發現了一些普遍存在的「壞味道 (Code Smell)」:delegate 普遍沒有使用 weak,而畫面的切換大量依賴 present,卻很少看到對應的 dismiss。
一個矛盾的現象是:這個專案已上架,但似乎沒有什麼毀滅性的閃退回報。這引發了我的思考,這些理論上的「架構缺陷」在實際運行中到底造成了多大的影響?
為了搞清楚這個問題,我決定去探索一個我此前並不熟悉的工具——Instruments,並使用其中的 Allocations 來記錄下這段探索之旅。
建立「實驗室」:用 Edge Case 測試來放大問題
直接在複雜的正式專案裡分析,對我這個 Instruments 工具新手來說太困難了。所以我做的第一件事,就是建立一個獨立、乾淨的測試專案。我的想法很簡單:如果我想看清楚問題,就必須先把它放大。
這個專案的目標很純粹:模擬並放大那兩種「壞味道」的影響。我設計了幾個按鈕,用來進行極端測試(Edge Case Test):
- 一次性在記憶體中創建大量「會洩漏的」 ViewController 實例。
- 一次性釋放它們。
- 用同樣的方式,創建和釋放「正常的」 ViewController 作為對照組。
初探 Allocations:看懂曲線背後的意義
在 Xcode 中,我透過 Product > Profile 啟動了 Instruments,選擇 Allocations,然後點擊紅色的錄製按鈕,我的測試正式開始。
以下是我在創建了上萬個物件後,進行釋放操作的對照圖:

上圖一:當我點擊「釋放 LeakyVC」後,記憶體曲線並沒有如預期般下降,Persistent 值依然很高。

圖二:我點擊「釋放 CorrectVC」後,記憶體曲線如預期般下降,Persistent 值也降了下來。
眼前的畫面讓我非常興奮!雖然我對 Persistent、Transient 這些術語還是一知半解,但這條沒有降下去的藍色曲線,就是最直觀的證據。它告訴我,有些東西被留下來了。
經過查詢,這兩個關鍵詞的意義:
- Persistent (持續存在):這是在我停止記錄時,依然存活在記憶體中的物件。它就是我洩漏的「贓物」。
- Transient (短暫存在):這是在記錄過程中,被創建出來、但又被成功釋放掉了的物件。
這個強烈的視覺對比,讓我第一次如此直觀地「看見」了記憶體洩漏。
從數據到根源:解讀 Allocations 列表
有了測試專案的經驗,我回頭分析正式專案的 Allocations 報告,這一次我不再迷茫。我知道要關注 Persistent 這一欄,並從中尋找元兇。

有兩行數據立刻吸引了我的注意:
- VM: Allocation 512.00 MiB:一個佔用了整整 512MB 虛擬記憶體的單一區塊。我了解到這通常和 WKWebView 為了效能而預留的空間有關。
- VM: UILabel (CALayer):這一項佔用了 136.84 MiB!並且我注意到,每次操作後它的 Persistent 值都會累加,但 Transient 欄位卻沒有對應增加。這強烈暗示著有大量的 View 沒有被釋放。
這兩條線索,與我最初在程式碼中看到的兩種「壞味道」完美地對應了起來:
- 那個洩漏的 512MB VM 區塊,很可能就是一個 WKWebView 因為 delegate 強引用循環而無法被釋放的結果。
- 而那龐大的 136MB UILabel 記憶體,則無疑是 ViewController 不斷 present 堆疊,導致巨量 View 被累積在記憶體中的直接後果。
藉助 Call Tree 功能(並進行如下圖的設定),我很快就定位到了那個被遺忘 weak 的 delegate,證實了我的猜想。

偵錯之外的兩個重要發現
這次的偵錯之旅,除了最終定位到問題根源,更有趣的是我在測試過程中得到的兩個重要發現。它們顛覆了我之前的一些刻板印象。
發現一:deinit 是最誠實的「生命指標」
deinit {
print("deinit -- 我被成功釋放了!")
}
我學到的第一個技巧,就是在 deinit 裡加上 print 語句。這個簡單的動作,成爲了我判斷物件生命週期是否正常的可靠指標。當記憶體洩漏發生時,無論我怎麼操作,控制台都是一片死寂;而修復問題後,每一次物件被釋放,控制台都會準確地打印出 deinit 的訊息。這給人一種安心的確定感,就像是親眼看到物件在揮手告別。
發現二:健康的釋放是有「成本」的,一次短暫的卡頓勝過虛假的流暢
在我的 Edge Case 測試中,一個現象讓我印象極其深刻:當我一次性釋放 10000 個正常物件時,App 的 UI 會出現一次短暫的、可感知的卡頓,隨後記憶體才成功下降。與此形成鮮明對比的是,在洩漏的版本中,「釋放」操作卻異常順暢。
這讓我恍然大悟:釋放記憶體是需要 CPU 在主執行緒上工作的。那一次短暫的卡頓,正是系統在努力呼叫上萬次 deinit、進行記憶體大掃除的證明。而那個「順暢」的假象,恰恰是因為 App 什麼都沒做,只是把清理工作跳過了,將問題永遠地留在了記憶體裡。
從此以後,我知道了不能只追求表面的流,更要理解背後發生的事。
結語:從陌生到熟悉的喜悅
這次的偵錯之旅,從我對一個舊專案「架構缺陷」的好奇心開始,最終以我對 iOS 記憶體管理更深的理解結束。
我明白了,那些看似沒有引發立即閃退的問題,並非無關緊要。它們會像溫水煮青蛙一樣,持續蠶食 App 的效能和穩定性,直到某一天在某個用戶的低階設備上,爆發為一次致命的閃退。
這次經歷讓我體會到,面對未知工具時,主動學習、動手實踐的過程本身,就是最有價值的收穫。現在,Instruments 的 Allocations 工具對我來說,不再是個令人望而生畏的面板,而是一個可以信賴的夥伴,幫助我深入洞察 App 的內部運作,寫出更健壯、更高效的程式碼。