合理的利用资源。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 内存深刻研究