Instruments - Leaks的使用

内存泄露

Memory that was allocated at some point, but was never released and is no longer referenced by your app. Since there are no references to it, there’s now no way to release it and the memory can’t be used again.

内存泄露指当一个对象或变量在使用完成后没有释放掉,这个对象一直占用着这部分内存, 直到应用停止。如果这种对象过多,内存就会耗尽,程序会因没有内存被杀死,即crash。内存泄露问题在 C++, C 和 Objective-C的 MRC 中是比较普遍的问题.。ARC中内存泄露问题较少,但是由于开发者的不注意,同样会出现内存泄露,比如:

  • 两个对象相互强引用
  • 代理
  • block
  • 通知
  • KVO
  • 定时器

注意:从理论上讲, 内存泄露是由对象或变量没有释放引起的, 但实践证明并非所有的未释放的对象或变量都会导致内存泄露, 这与硬件环境和操作系统系统环境有关。

查找泄漏点

在 Xcode 中, 共提供了两种工具帮助

  • Analyze

静态分析工具: 可以通过Product ->Analyze菜单项启动

Analyze主要分析以下四种问题:

1、逻辑错误:访问空指针或未初始化的变量等;
2、内存管理错误:如内存泄漏等;
3、声明错误:从未使用过的变量;
4、API调用错误:未包含使用的库和框架。

这里使用Analyze静态分析查找出来的泄漏点,称之为"可疑泄漏点"。之所以称之为"可疑泄漏点",是因为这些点未必一定泄露,确认这些点是否泄露, 还要通过Instruments动态分析工具的 LeaksAllocations跟踪模板。 Analyze静态分析只是一个理论上的预测过程.

  • Instruments

Instruments可以帮我们了解到应用程序使用内存的几个方面:

  • 全局内存使用情况(Overall Memory Use)

从全局的角度监测应用程序的内存使用情况,捕捉非预期的或大幅度的内存增长

  • 内存泄露(Leaked memory)

未被你的程序引用,同时也不能被使用或释放的内存

  • 废弃内存(Abandoned memory)

被你的程序引用,但是没什么用的内存

  • 僵尸对象(Zombies)

僵尸对象指的是对应的内存已经被释放并且不再会使用到,但是你的程序却在某处依然有指向它的引用。在 iOS 中有一个NSZombie机制,这个是为了内存调试的目的而设计的一种机制。在这个机制下,当你NSZombieEnabled为 YES 时,当一个对应的引用计数减为 0 时,这个对象不会被释放,当这个对象再收到任何消息时,它会记录一条warning,而不是直接崩溃,以方便我们进行程序调试。

Leaks

查找内存泄露的过程

1、在Xcode中对当前的项目执行Profile (Command-I),并在打开的对话框中选择Leaks这个模板:

屏幕快照 2019-08-05 下午2.17.15.png

也可以通过Xcode->Open Developer Tool->Instrument启动Instruments

屏幕快照 2019-08-05 下午3.41.10.png

2、进入Instruments后,选择正确的设备和应用程序。打开界面如下

屏幕快照 2019-08-05 下午2.27.48.png

Instruments中,虽然选择了Leaks模板,但默认情况下也会添加Allocations模板。基本上凡是内存分析都会使用Allocations模板, 它可以监控内存分布情况。

3、点击红色按钮运行应用程序,我们可以看到如下界面:

屏幕快照 2019-08-05 下午2.39.44.png

4、选择Leak Checks来查看内存泄露

Leaks

屏幕快照 2019-08-05 下午2.42.20.png

其中,绿色勾表示运行正常,没有内存泄露,如果有泄露,会自动显示红色x

注意:显示红色x并不代表一定就有内存泄露,而且并不一定每次操作都能看到正确定位内存泄露部分。因为ARC 时代更常见的内存泄露是循环引用导致的Abandoned memory,而 Leaks 工具只负责检测 Leaked memory,应用有限。

Cycles & Reboots

屏幕快照 2019-08-05 下午2.45.04.png

Call Tree

屏幕快照 2019-08-05 下午2.46.07.png

Canll Tree部分

  • Separate By Thread

线程分离,只有这样才能在调用路径中能够清晰看到占用CPU最大的线程

  • Invert Call Tree

从上到下跟踪堆栈信息.这个选项可以快捷的看到方法调用路径最深方法占用CPU耗时,比如FuncA{FunB{FunC}},勾选后堆栈以C->B->A把调用层级最深的C显示最外面

  • Hide System Libraries

这个就更有用了,勾选后耗时调用路径只会显示app耗时的代码,性能分析普遍我们都比较关系自己代码的耗时而不是系统的。基本是必选项,注意有些代码耗时也会纳入系统层级,可以进行勾选前后前后对执行路径进行比对会非常有用

  • Top Functions

按耗时降序排列

  • Flatten Recursion(一般不选)

选上它会将调用栈里递归函数作为一个入口

简单的方式可以快速勾选右边Call Tree中Separate by Thread和Hide System Libraries两个选项

实际Demo

写一个简单的Demo来实际查看一下效果

  • 创建工程,在Main.storyboard中选择NavigationController作为根视图控制器,在ViewController上添加一个UITableView并设置delegate、dataSource
  • 添加一个DetailViewController,实现代理方法显示内容,点击cell进入详情页面
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    var titles: [String] = []
    var images: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

        for i in 0..<30 {
            titles.append("cell\(i)")
            images.append("imageString")
        }
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return titles.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        return cell
    }
}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        cell.textLabel?.text = titles[indexPath.row]
        cell.imageView?.image = UIImage(named: images[indexPath.row])
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let vc = DetailViewController()
        self.navigationController?.pushViewController(vc, animated: true)
    }
}
  • 添加一个新的Swift文件,创建PersonPet两个类
import Foundation

class Person {
    let name: String
    var pet: Pet?

    init(name: String) {
        self.name = name
    }
}

class Pet {
    let name: String
    var onwer: Person?

    init(name: String) {
        self.name = name
    }
}
  • DetailViewController中实现循环引用,这里包括两个地方,一个是定时器的循环引用,一个是对象之间的循环引用
import UIKit

class DetailViewController: UIViewController {
    var jack:Person!
    var dog: Pet!

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.white

        jack = Person(name: "Jack")
        dog = Pet(name: "dog")
        jack.pet = dog
        dog.onwer = jack
    }

    deinit {
       print("deinit")
    }
}

运行Leaks来查看

运行程序之后,点击进入详情来回几次即可

Leaks模式

屏幕快照 2019-08-05 下午5.57.16.png

看不出什么很有用的信息

Cycles & Reboots模式

屏幕快照 2019-08-05 下午5.56.42.png

这里还是非常明显的,简单清晰易读,PersonPet相互引用

Call Tree模式

屏幕快照 2019-08-05 下午5.56.57.png

可以通过双击Symbol Name来定位代码,也可以选择对应的行,右键Reveal In Xcode

Debug Memory Graph

直接使用Xcode自带的Debug Memory Graph来查看内存情况。运行程序,点击cell来回操作几次,然后点击Debug Memory Graph

屏幕快照 2019-08-05 下午4.40.55.png

查看结果

屏幕快照 2019-08-05 下午5.54.22.png

可以明显的看到对象之间的循环引用

第三方内存查找库

FBRetainCycleDetectorfacebook开源的一个用来检测对象是否有强引用循环的静态库。

MLeaksFinder 提供了内存泄露检测更好的解决方案。只需要引入MLeaksFinder,就可以自动在 App运行过程检测到内存泄露的对象并立即提醒,无需打开额外的工具,也无需为了检测内存泄露而一个个场景去重复地操作。MLeaksFinder 目前能自动检测UIViewControllerUIView对象的内存泄露,而且也可以扩展以检测其它类型的对象。

MLeaksFinder 的使用很简单,参照 https://github.com/Zepo/MLeaksFinder,基本上就是把 MLeaksFinder 目录下的文件添加到你的项目中,就可以在运行时(debug 模式下)帮助你检测项目里的内存泄露了,无需修改任何业务逻辑代码,而且只在 debug 下开启,完全不影响你的 release 包。

实现原理可以看MLeaksFinder:精准 iOS 内存泄露检测工具

iOS内存泄漏自动检测工具PLeakSniffer

推荐使用第三方库来监测内存泄漏,开发的时候快速定位,节约时间

参考

Memory Usage Performance Guidelines
Profile your app’s memory usage
Instruments Tutorial with Swift: Getting Started

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容