内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

栏目: IOS · 发布时间: 5年前

内容简介:这么用,在重点测试的界面,多操作,然后退出。重复几次。确认系统缓存已初始化。 退出重点测的界面后,开内存图, 如果内存释放的干净,就没什么 retain cycle 等内存泄漏。
内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

这么用,

在重点测试的界面,多操作,然后退出。

重复几次。确认系统缓存已初始化。 退出重点测的界面后,开内存图, 如果内存释放的干净,就没什么 retain cycle 等内存泄漏。

内存图自带断点效果,会暂停 app 的运行

可以看到此刻存在的所有对象。

环节短的循环引用,明显可见,找起来很快。

通过内存图,左边列表中,可以看到当前的所有对象,以及它们的数量。

最关心的就是感叹号,代表异常, 就是内存泄漏, 一般是 Retain Cycle

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

本文 Demo ,可见系统的代理 AppDelegate 实例, 相关 ViewController . 可看到图片视图有 24个。

中间大片的区域是对象的内存图,可看到他们是怎么关联的。可参考下

左边栏的右下方按钮,可以直接筛选出内存有错误的对象,方便找出内存泄漏的对象

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

可看出本文 Demo 内存泄漏严重。左边栏,点开几个带感叹号的,看情况。

右边栏,有一些具体信息

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

photo 照片模型对象,持有一个 location 位置的模型对象, location 位置的模型对象,持有一个对象,

那对象,又持有 photo 照片模型对象。

三个对象,构成了一个强引用的圈, retain cycle

发现问题了,解决就是改代码 很熟悉,直接改。

可以全局搜关键字,本文 demo 搜 .location

可以根据右边栏的信息找,

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

知道是哪个类,又有一个 closure 对象

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

可找到错误代码

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 的调试计量 工具 很强大,调试内存的时候,可切换调试视图层级等

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

左边栏的右上方的按钮,可以切换调试的选项, 内存转 UI, 内存转线程

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

通过使用 Xcode 内存图,内存泄漏少了很多。 重复操作三五次,又发现一个内存泄漏

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

对象结点很多,看图挺复杂的

可以用 Instruments 的 Leaks

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

Leaks 自带两个模版 Allocation 和 Leaks,

Allocation 模版对 app 运行过程中分配的所有对象的内存,都追踪到了。 上方的时间线展示了,已经分配了多少兆的内存。

All Heap & Anonymous VM, 所有堆上的内存,和虚拟内存 ( WWDC 2018/416 , 讲的比较详细)

下方的标记按钮,可以做分代标记

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

Leaks 模版会检查 app 所有的内存,找出泄漏的对象 ( 释放不了的对象 )

Instruments 的内存检查机制是,默认每隔 10 秒钟,自发的取一个内存快照分析

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

反复操作,找到第一个 Leaks, 可以暂停下

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

下方的 Leaks 详情表中,头部的 Leaks 按钮,有三个选项, 默认选项就是第一个, Leaks, 展示了所有内存泄漏的对象。

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

下方的右边栏就是更多信息,展示了详情界面每一列对象的进一步的资料

Leaks 详情表中,每一列对象,有一个灰色的箭头按钮,

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

点进去,可以看引用计数的增减日志

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

一般先看看第二个 Cycles & Roots, 又是一张内存图

photoModel 是循环圈的根结点,与左边的对象结点列表一致

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

有用的是第三个选项 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.

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

按住 Alt 键,点击方法名称左边的小三角,可以展开调用栈。

内存二三事: Xcode 内存图、Instruments 可视化检测循环引用

又看到了这个方法 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.

检查项目中的循环引用,通常使用分代式分析 ( Generational Analysis )

先记录一个内存使用的基线 A ( 当前使用场景, 建议用重点测的场景前的那一个 ),

进入一个场景 ( Controller 重点测的场景), 打个标 ( 记录现在的内存使用情况 ) B ,

再退出该场景,再打一个标 C。

如果 A < B , A = C , 正常,内存回收的不错。 如果 A < B <= C , 异常,内存很可能泄漏了

换句话,套路很简单,设立内存基线,点击进入新界面,(操作一下,滚一滚) 然后弹出,内存往往会先升后降。

这种操作,需要重复几次。找出必然。确认系统缓存已初始化,在运行。

( 有点类似苹果的单元测试算函数执行时间,跑一遍,就是运行了好几次的函数,取的平均值。 )

这里有一个很经典的面试题:

app 发布前,一般会系统检查循环引用,内存泄漏,怎么处理呢?

( 换个说法, 怎么分析 app 堆的快照? )

方案见前文


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

动手玩转Scratch2.0编程

动手玩转Scratch2.0编程

马吉德·马吉 (Majed Marji) / 电子工业出版社 / 2015-10-1 / CNY 69.00

Scratch 是可视化的编程语言,其丰富的学习环境适合所有年龄阶段的人。利用它可以制作交互式程序、富媒体项目,包括动画故事、读书报告、科学实验、游戏和模拟程序等。《动手玩转Scratch2.0编程—STEAM创新教育指南》的目标是将Scratch 作为工具,教会读者最基本的编程概念,同时揭示Scratch 在教学上的强大能力。 《动手玩转Scratch2.0编程—STEAM创新教育指南》共......一起来看看 《动手玩转Scratch2.0编程》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具