内存二三事: 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 内存深刻研究

相关文章
相关标签/搜索