小結下,記憶體管理的語義:
- 需要該物件的時候,他就得在。不需要他的時候,他最好被釋放了。
合理的利用資源。
- 需要該物件的時候,他不在,釋放早了。
野指標問題,用殭屍物件除錯
給他發訊息,程式會崩,EXC_BAD_INSTRUCTION
- 不需要該物件的時候,他還在。記憶體可能洩漏了。
一般是迴圈引用 ( retain cycle )
iOS 的記憶體分析,工具挺多
可以使用 Xcode 的 Debug 工具,記憶體圖( 點一下,斷點旁邊 )
這麼用,
在重點測試的介面,多操作,然後退出。
重複幾次。確認系統快取已初始化。 退出重點測的介面後,開記憶體圖, 如果記憶體釋放的乾淨,就沒什麼 retain cycle 等記憶體洩漏。
記憶體圖自帶斷點效果,會暫停 app 的執行
可以看到此刻存在的所有物件。
環節短的迴圈引用,明顯可見,找起來很快。
通過記憶體圖,左邊列表中,可以看到當前的所有物件,以及它們的數量。
最關心的就是感嘆號,代表異常, 就是記憶體洩漏, 一般是 Retain Cycle
本文 Demo ,可見系統的代理 AppDelegate 例項, 相關 ViewController . 可看到圖片檢視有 24個。
中間大片的區域是物件的記憶體圖,可看到他們是怎麼關聯的。可參考下
左邊欄的右下方按鈕,可以直接篩選出記憶體有錯誤的物件,方便找出記憶體洩漏的物件
可看出本文 Demo 記憶體洩漏嚴重。左邊欄,點開幾個帶感嘆號的,看情況。
右邊欄,有一些具體資訊
photo 照片模型物件,持有一個 location 位置的模型物件, location 位置的模型物件,持有一個物件,
那物件,又持有 photo 照片模型物件。
三個物件,構成了一個強引用的圈, retain cycle
發現問題了,解決就是改程式碼 很熟悉,直接改。
可以全域性搜關鍵字,本文 demo 搜 .location
可以根據右邊欄的資訊找,
知道是哪個類,又有一個 closure 物件
可找到錯誤程式碼
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 的除錯計量工具很強大,除錯記憶體的時候,可切換除錯檢視層級等
左邊欄的右上方的按鈕,可以切換除錯的選項, 記憶體轉 UI, 記憶體轉執行緒
通過使用 Xcode 記憶體圖,記憶體洩漏少了很多。 重複操作三五次,又發現一個記憶體洩漏
物件結點很多,看圖挺複雜的
可以用 Instruments 的 Leaks
Leaks 自帶兩個模版 Allocation 和 Leaks,
Allocation 模版對 app 執行過程中分配的所有物件的記憶體,都追蹤到了。 上方的時間線展示了,已經分配了多少兆的記憶體。
All Heap & Anonymous VM, 所有堆上的記憶體,和虛擬記憶體 ( WWDC 2018/416 , 講的比較詳細)
下方的標記按鈕,可以做分代標記
Leaks 模版會檢查 app 所有的記憶體,找出洩漏的物件 ( 釋放不了的物件 )
Instruments 的記憶體檢查機制是,預設每隔 10 秒鐘,自發的取一個記憶體快照分析
反覆操作,找到第一個 Leaks, 可以暫停下
下方的 Leaks 詳情表中,頭部的 Leaks 按鈕,有三個選項, 預設選項就是第一個, Leaks, 展示了所有記憶體洩漏的物件。
下方的右邊欄就是更多資訊,展示了詳情介面每一列物件的進一步的資料
Leaks 詳情表中,每一列物件,有一個灰色的箭頭按鈕,
點進去,可以看引用計數的增減日誌
一般先看看第二個 Cycles & Roots, 又是一張記憶體圖
photoModel 是迴圈圈的根結點,與左邊的物件結點列表一致
有用的是第三個選項 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.
按住 Alt 鍵,點選方法名稱左邊的小三角,可以展開呼叫棧。
又看到了這個方法 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 記憶體深入研究