內存二三事: Xcode 內存圖、Instruments 可視化檢測循環引用

小結下,內存管理的語義:

  • 須要該對象的時候,他就得在。不須要他的時候,他最好被釋放了。

合理的利用資源。git

  • 須要該對象的時候,他不在,釋放早了。

野指針問題,用殭屍對象調試github

給他發消息,程序會崩,EXC_BAD_INSTRUCTION面試

  • 不須要該對象的時候,他還在。內存可能泄漏了。

通常是循環引用 ( retain cycle )api


iOS 的內存分析,工具挺多緩存

可使用 Xcode 的 Debug 工具,內存圖( 點一下,斷點旁邊 )

0

這麼用,bash

在重點測試的界面,多操做,而後退出。閉包

重複幾回。確認系統緩存已初始化。 退出重點測的界面後,開內存圖, 若是內存釋放的乾淨,就沒什麼 retain cycle 等內存泄漏。app

內存圖自帶斷點效果,會暫停 app 的運行ide

能夠看到此刻存在的全部對象。函數

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

經過內存圖,左邊列表中,能夠看到當前的全部對象,以及它們的數量。

最關心的就是感嘆號,表明異常, 就是內存泄漏, 通常是 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 內存深刻研究

相關文章
相關標籤/搜索