記憶體二三事: Xcode 記憶體圖、Instruments 視覺化檢測迴圈引用

鄧輕舟發表於2018-12-16

小結下,記憶體管理的語義:

  • 需要該物件的時候,他就得在。不需要他的時候,他最好被釋放了。

合理的利用資源。

  • 需要該物件的時候,他不在,釋放早了。

野指標問題,用殭屍物件除錯

給他發訊息,程式會崩,EXC_BAD_INSTRUCTION

  • 不需要該物件的時候,他還在。記憶體可能洩漏了。

一般是迴圈引用 ( retain cycle )


iOS 的記憶體分析,工具挺多

可以使用 Xcode 的 Debug 工具,記憶體圖( 點一下,斷點旁邊 )

0

這麼用,

在重點測試的介面,多操作,然後退出。

重複幾次。確認系統快取已初始化。 退出重點測的介面後,開記憶體圖, 如果記憶體釋放的乾淨,就沒什麼 retain cycle 等記憶體洩漏。

記憶體圖自帶斷點效果,會暫停 app 的執行

可以看到此刻存在的所有物件。

環節短的迴圈引用,明顯可見,找起來很快。

通過記憶體圖,左邊列表中,可以看到當前的所有物件,以及它們的數量。

最關心的就是感嘆號,代表異常, 就是記憶體洩漏, 一般是 Retain Cycle

0

本文 Demo ,可見系統的代理 AppDelegate 例項, 相關 ViewController . 可看到圖片檢視有 24個。

中間大片的區域是物件的記憶體圖,可看到他們是怎麼關聯的。可參考下

左邊欄的右下方按鈕,可以直接篩選出記憶體有錯誤的物件,方便找出記憶體洩漏的物件

1

可看出本文 Demo 記憶體洩漏嚴重。左邊欄,點開幾個帶感嘆號的,看情況。

右邊欄,有一些具體資訊

11

photo 照片模型物件,持有一個 location 位置的模型物件, location 位置的模型物件,持有一個物件,

那物件,又持有 photo 照片模型物件。

三個物件,構成了一個強引用的圈, retain cycle


發現問題了,解決就是改程式碼 很熟悉,直接改。

可以全域性搜關鍵字,本文 demo 搜 .location

可以根據右邊欄的資訊找,

0

知道是哪個類,又有一個 closure 物件

0

可找到錯誤程式碼

func reverseGeocode(locationForPhoto photoModel: PhotoModel) {
        photoModel.location?.reverseGeocodedLocation(completion: {   (locationModel) in
            self.photoLocationLabel.attributedText = photoModel.locationAttributedString(withFontSize: 14.0)
           
        })
}

複製程式碼

photoModel 有一個 location 的屬性,location 持有一個匿名函式 closure. 這個 closure 又引用了 photoModel。

不知道這個 closure 有沒有 retain 該 photoModel,

點進方法看, 這是一個逃逸閉包,賦給了 LocationModel 的 placeMarkCallback 屬性,強引用

func reverseGeocodedLocation(completion: @escaping ((LocationModel) -> Void)) {
        if placemark != nil {
            completion(self)
        }
        else {
            // 檢視 completion
            placeMarkCallback = completion
            if !placeMarkFetchInProgress {
                beginReverseGeocodingLocationFromCoordinates()
            }
        }
    }
複製程式碼

與 Xcode 記憶體圖檢查到的一致。


解決迴圈引用,一般加 weak

ARC , 自動引用記數, iOS 用來管理記憶體的。 迴圈引用,retain cycle, 是 ARC 搞不定的地方

一個物件的引用記數, 就是有多少個其他的物件,持有對他的引用。

( 就是有多少個其他的物件,有指標指向他)

當這個物件的引用計數為 0, iOS 的 ARC 記憶體機制知道這個物件不必存在了,會找一個合適的時機釋放。

迴圈引用,多個物件相互引用,形成了一個圈( 強引用的鏈路 )

迴圈引用,問題很嚴重,記憶體洩漏了 ( 打個比方: 你找 iOS 系統借了錢,少還一大截。人家系統沒說什麼, 心裡都記著 )

加 weak, ARC 就明白了, ( 因為 weak 是弱引用,不會增加該物件的引用記數。 直接寫,隱含了一個 strong 的語義,預設 retain , 該物件的引用記數 + 1 )

鏈路就斷了,記憶體回收成功。


Swift 的 closure 中,可以新增一個弱引用列表。 這個捕獲列表可以讓指定的屬性弱引用。 closure 使用弱引用,就好


func reverseGeocode(locationForPhoto photoModel: PhotoModel) {
        photoModel.location?.reverseGeocodedLocation(completion: {  [ weak photoModel] (locationModel) in
            self.photoLocationLabel.attributedText = photoModel?.locationAttributedString(withFontSize: 14.0)
           
        })
    }
複製程式碼

Xcode 的除錯計量工具很強大,除錯記憶體的時候,可切換除錯檢視層級等

1

左邊欄的右上方的按鈕,可以切換除錯的選項, 記憶體轉 UI, 記憶體轉執行緒

2


通過使用 Xcode 記憶體圖,記憶體洩漏少了很多。 重複操作三五次,又發現一個記憶體洩漏

0

物件結點很多,看圖挺複雜的

可以用 Instruments 的 Leaks

0

Leaks 自帶兩個模版 Allocation 和 Leaks,

Allocation 模版對 app 執行過程中分配的所有物件的記憶體,都追蹤到了。 上方的時間線展示了,已經分配了多少兆的記憶體。

All Heap & Anonymous VM, 所有堆上的記憶體,和虛擬記憶體 ( WWDC 2018/416 , 講的比較詳細)

下方的標記按鈕,可以做分代標記

0

Leaks 模版會檢查 app 所有的記憶體,找出洩漏的物件 ( 釋放不了的物件 )

Instruments 的記憶體檢查機制是,預設每隔 10 秒鐘,自發的取一個記憶體快照分析

0


反覆操作,找到第一個 Leaks, 可以暫停下

0

下方的 Leaks 詳情表中,頭部的 Leaks 按鈕,有三個選項, 預設選項就是第一個, Leaks, 展示了所有記憶體洩漏的物件。

0

下方的右邊欄就是更多資訊,展示了詳情介面每一列物件的進一步的資料

Leaks 詳情表中,每一列物件,有一個灰色的箭頭按鈕,

0

點進去,可以看引用計數的增減日誌

0

一般先看看第二個 Cycles & Roots, 又是一張記憶體圖

photoModel 是迴圈圈的根結點,與左邊的物件結點列表一致

1

有用的是第三個選項 Call Tree , 呼叫樹

與 Time Profiler 的 Call Tree 不一樣,

Time Profiler 的 Call Tree 採集的是應用中所有的方法呼叫, Leaks 的 Call Tree 採集的是分配記憶體與記憶體洩漏相關的方法呼叫。

Call Tree 的選項一般勾選 Hide System Libraries 和 Separate by Thread.

Hide System Libraries , 隱藏系統的方法。系統的方法改不了,是黑盒,參考意義有限。

Separate by Thread. 將方法堆疊,按執行緒分開。一般出問題多在主執行緒,優先看 main thread.

0

按住 Alt 鍵,點選方法名稱左邊的小三角,可以展開呼叫棧。

1

又看到了這個方法 func reverseGeocode(locationForPhoto photoModel: PhotoModel)

再檢查下

func reverseGeocode(locationForPhoto photoModel: PhotoModel) {
        photoModel.location?.reverseGeocodedLocation(completion: {  [ weak photoModel] (locationModel) in
            self.photoLocationLabel.attributedText = photoModel?.locationAttributedString(withFontSize: 14.0)
           
        })
    }
複製程式碼

self 是一個 CatPhotoTableViewCell 例項,self 持有 photoModel 屬性,

( 函式裡面的 photoModel, 使用的是 func updateCell(with photo: PhotoModel?) { 方法中傳入的 self 的 photoModel 屬性)

photoModel 持有 location 屬性, location 屬性持有一個逃逸閉包, 該逃逸閉包持有 self.

之前用 weak 處理了三物件的迴圈引用,現在有一個四物件的迴圈引用。

四物件的迴圈引用中 photoModel 在之前的處理中,已經弱引用了。本來好像沒什麼問題的。

估計系統沒及時釋放的 weak 的 photoModel,又洩漏了。

本文中,採用 Xcode 記憶體圖,難以復現。有時候有。


解決就是再加一個 weak.


func reverseGeocode(locationForPhoto photoModel: PhotoModel) {
        photoModel.location?.reverseGeocodedLocation(completion: {  [weak self, weak photoModel] (locationModel) in
            self?.photoLocationLabel.attributedText = photoModel?.locationAttributedString(withFontSize: 14.0)
           
        })
    }

複製程式碼

檢查專案中的迴圈引用,通常使用分代式分析 ( Generational Analysis )

先記錄一個記憶體使用的基線 A ( 當前使用場景, 建議用重點測的場景前的那一個 ),

進入一個場景 ( Controller 重點測的場景), 打個標 ( 記錄現在的記憶體使用情況 ) B ,

再退出該場景,再打一個標 C。

如果 A < B , A = C , 正常,記憶體回收的不錯。 如果 A < B <= C , 異常,記憶體很可能洩漏了

換句話,套路很簡單,設立記憶體基線,點選進入新介面,(操作一下,滾一滾) 然後彈出,記憶體往往會先升後降。

這種操作,需要重複幾次。找出必然。確認系統快取已初始化,在執行。

( 有點類似蘋果的單元測試算函式執行時間,跑一遍,就是執行了好幾次的函式,取的平均值。 )


這裡有一個很經典的面試題:

app 釋出前,一般會系統檢查迴圈引用,記憶體洩漏,怎麼處理呢?

( 換個說法, 怎麼分析 app 堆的快照? )

方案見前文


相關程式碼: github.com/BoxDengJZ/I…

更多資料: 視訊教程,practical-instruments


同質部落格: Memory

擴充套件閱讀:

命令列工具 vmmap - 檢視虛擬記憶體 : WWDC 2018:iOS 記憶體深入研究

相關文章