合理的利用資源。git
野指針問題,用殭屍對象調試github
給他發消息,程序會崩,EXC_BAD_INSTRUCTION面試
通常是循環引用 ( retain cycle )api
iOS 的內存分析,工具挺多緩存
這麼用,bash
在重點測試的界面,多操做,而後退出。閉包
重複幾回。確認系統緩存已初始化。 退出重點測的界面後,開內存圖, 若是內存釋放的乾淨,就沒什麼 retain cycle 等內存泄漏。app
內存圖自帶斷點效果,會暫停 app 的運行ide
能夠看到此刻存在的全部對象。函數
環節短的循環引用,明顯可見,找起來很快。
經過內存圖,左邊列表中,能夠看到當前的全部對象,以及它們的數量。
最關心的就是感嘆號,表明異常, 就是內存泄漏, 通常是 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 內存圖檢查到的一致。
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 內存圖,內存泄漏少了不少。 重複操做三五次,又發現一個內存泄漏
對象結點不少,看圖挺複雜的
Leaks 自帶兩個模版 Allocation 和 Leaks,
Allocation 模版對 app 運行過程當中分配的全部對象的內存,都追蹤到了。 上方的時間線展現了,已經分配了多少兆的內存。
All Heap & Anonymous VM, 全部堆上的內存,和虛擬內存 ( WWDC 2018/416 , 講的比較詳細)
下方的標記按鈕,能夠作分代標記
Leaks 模版會檢查 app 全部的內存,找出泄漏的對象 ( 釋放不了的對象 )
Instruments 的內存檢查機制是,默認每隔 10 秒鐘,自發的取一個內存快照分析
下方的 Leaks 詳情表中,頭部的 Leaks 按鈕,有三個選項, 默認選項就是第一個, Leaks, 展現了全部內存泄漏的對象。
下方的右邊欄就是更多信息,展現了詳情界面每一列對象的進一步的資料
Leaks 詳情表中,每一列對象,有一個灰色的箭頭按鈕,
點進去,能夠看引用計數的增減日誌
photoModel 是循環圈的根結點,與左邊的對象結點列表一致
與 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 堆的快照? )
方案見前文
更多資料: 視頻教程,practical-instruments
同質博客: Memory
擴展閱讀:
命令行工具 vmmap - 查看虛擬內存 : WWDC 2018:iOS 內存深刻研究