内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

6.内存泄露监控

2018-06-08 19:46 出处:清屏网 人气: 评论(0

内存泄漏是一个老生常谈的问题了,在iOS 5.0之前,内存管理是通过MRC机制管理的,笔者刚开始接触iOS开发的时候参与的第一个项目也还在采用MRC管理机制,MRC秉承着‘谁创建,谁释放,谁引用,谁管理’的理念来管理内存,所以那个时代的iOS程序员还是比较苦逼的,一不小心就导致内存泄露或过度释放了。虽然在iOS 5.0版本之后Apple加入了ARC机制,但是互相引用关系比较复杂时,内存泄漏还是很容易存在。

Apple提供了Instrument工具,平常我们都会用其中的Leaks/Allocations来进行内存泄漏的排查,但是Instrument还是有很多不便,这个我们会在后续介绍,本章节要介绍的重点并不是Instrument,而是通过自己编写代码去监控发现内存泄漏。

这次我们会介绍两个代码轮子,一个是微信阅读的MLeaksFinder方案,另外一个是MrPeak的PLeakSniffer方案。GodEye采用的是MrPeak的PLeakSniffer方案,GodEye是一个Swift项目,因此笔者将PLeakSniffer翻译成了Swift的版本: LeakEye-- https://github.com/zixun/LeakEye

6.1 微信阅读MLeaksFinder方案

MLeaksFinder是微信阅读团队研发的一款监控内存泄漏的工具,开源在微信阅读团队成员Zepo名下: https://github.com/Zepo/MLeaksFinder 。

以下文章主要摘自微信阅读团队官方博客: https://wereadteam.github.io/2016/02/22/MLeaksFinder

Instrument的局限

Leaks

Leaks是Instrument工具中的一个模块,可以检测内存泄漏。App的内存分三类Leaked memory、Abandoned memory和Cached memory。

其中 Leaked memory 和 Abandoned memory 都属于应该释放而没释放的内存,都是内存泄露,而 Leaks 工具只负责检测 Leaked memory,而不管 Abandoned memory。在 MRC 时代 Leaked memory 很常见,因为很容易忘了调用 release,但在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露,应用有限。

Allocations

对于上文提到的Abandoned memory,我们可以用Instrument的Allocations来检测出来。检测方法是用 Mark Generation 的方式,当你每次点击 Mark Generation 时,Allocations 会生成当前 App 的内存快照,而且 Allocations 会记录从上回内存快照到这次内存快照这个时间段内,新分配的内存信息。

但是采用这种方法来发现内存泄漏还是很不方便的,首先你得打开Allocations,然后,你得一个个场景去重复的操作,我们无法及时得知泄漏,得专门做一遍上述操作,十分繁琐。

内存泄漏检测工具的意义

在我们讨论这类工具的意义之前,我们先得明确一点:

如果不使用Instrument当中的Leak检测工具,并没有什么轻易的100%精准的内存泄漏检测方式。

但这类工具还是有其存在价值的,内存泄漏的危害不用赘述,如果有一款工具能在80%的场景下检测出可能的内存泄漏,而且这种检测并不会带来任何副作用(不影响生产环境代码),为什么不使用它呢。

大部分人都低估了他们写代码时导致意外内存泄漏的可能性。Retain Cycle,Block强引用,NSTimer释放不当,这些常见的错误还是很容易出现在我们的代码里,Instrument每使用一次要费些精力,适合做定期的大排查。平常时候就更适合用MLeaksFinder,PLeakSniffer这类工具来做实时监控,提供免费建议。

MLeaksFinder

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

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

当发生内存泄露时,MLeaksFinder 会中断言,并准确的告诉你哪个对象泄露了。这里设计为中断言而不是打日志让程序继续跑,是因为很多人不会去看日志,断言则能强制开发者注意到并去修改,而不是犯拖延症。

中断言时,控制台会有如下提示,View-ViewController stack 从上往下看,该 stack 告诉你,MyTableViewController 的 UITableView 的 subview UITableViewWrapperView 的 subview MyTableViewCell 没被释放。而且,这里我们可以肯定的是 MyTableViewController,UITableView,UITableViewWrapperView 这三个已经成功释放了。

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Possibly Memory Leak.
In case that MyTableViewCell should not be dealloced, override -willDealloc in MyTableViewCell by returning NO.
View-ViewController stack: (
    MyTableViewController,
    UITableView,
    UITableViewWrapperView,
    MyTableViewCell
)'

从 MLeaksFinder 的使用方法可以看出,MLeaksFinder 具备以下优点:

1.使用简单,不侵入业务逻辑代码,不用打开 Instrument

2.不需要额外的操作,你只需开发你的业务逻辑,在你运行调试时就能帮你检测

3.内存泄露发现及时,更改完代码后一运行即能发现(这点很重要,你马上就能意识到哪里写错了)

4.精准,能准确地告诉你哪个对象没被释放

原理

MLeaksFinder 一开始从 UIViewController 入手。我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。

- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}
- (void)assertNotDealloc {
     NSAssert(NO, @“”);
}

这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功, weakSelf 就指向 nil ,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了), -assertNotDealloc 就会被调用中断言。这样,当一个 UIViewControllerpopdismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view ,依次调 -willDealloc ,若3秒后没被释放,就会中断言。

更详细的内容可以移步文章开头中提到的MLeaksFinder开源库或者微信阅读官方博客查看。微信阅读团队后续也对M做了一次更新,添加了一些新特性: http://wereadteam.github.io/2016/07/20/MLeaksFinder2

6.2 MrPeak's PLeakSniffer方案

PLeakSniffer是MrPeak大神的一款自动检测内存泄漏的作品,开源在: https://github.com/music4kid/PLeakSniffer , MrPeak大神也写了一篇博文详细的介绍了制作PLeakSniffer的原因以及工作原理。GodEye的内存泄漏模块LeakEye: https://github.com/zixun/LeakEye 也是借鉴PLeakSniffer的方案,用Swift实现。

该篇博文会根据MrPeak介绍PLeakSniffer的博文以及LeakEye的实现来介绍这种方案的实现原理。

背景

PLeakSniffer的方案一直在MrPeak的构思中,后来微信阅读团队开源了他们的内存泄漏检测工具MLeaksFinder,MrPeak在仔细阅读源码,了解实现思路后发现和她自己最初的想法并不相同,最后付诸于代码,也就但是了这款功能类似的内存泄漏检测工具PLeakSniffer。

而LeakEye是笔者在仔细阅读PLeakSniffer源码,理解原理后用Swift重写的一款内存泄漏检测工具,基本原理与PLeakSniffer一致。

再造轮子的原因

MrPeak在完成PLeakSniffer之后,用MLeaksFinder和PLeakSniffer分别跑了一遍自己的工程,查出了相同的内存泄漏,思路迥异的代码抵达了相同的终点,这也是一件非常有趣的事情。

微信阅读团队的MLeaksFinder现阶段能查处UIViewController和UIView的泄漏,MrPeak早先的想法还能递归的查出UIViewController之下所有Property的泄漏,并在PLeakSniffer及他公司项目中得到了初步的验证,这算是对MLeaksFinder功能的一个小补充。

而LeakEye是GodEye的一个子模块,整个项目都是用Swift编写的,笔者也非常喜欢PLeakSniffer的解决思路,就讲其翻译成Swift版本,开源给开发者社区。

实现思路

PLeakSniffer方案的思路是这样的,我们绝大多数时候都是在编写UIViewController,那么UIViewController就像是我们的一个根节点,他持有并管理着很多的子节点对象,这些子节点的生命周期都依赖于Controller,Controller释放的时候,他们也会随着释放。

PLeakSniffer方案假设如果Controller释放了,但是其曾经持有过的对象如果还存在,那么这些子对象就是泄漏的可疑目标。

当然这个假设并不是一个100%适用的真理,不同工程师编写代码的方式风格差别很大,有些会把某些UIViewController做成单例(个人觉得这不是个好主意。。),有些会把某些View缓存起来(即使Controller已被释放),还会有其他考虑不到的场景。但在80%以上的场景,我们在Controller结束生命周期之后会将其持有的资源一并释放。这时候PLeakSniffer可以发挥用处,给你一些免费的泄漏建议。

如何拿到Controller持有的对象

思路很简单,首先我们需要hook掉我们的 viewDidAppear 方法,然后去调用我们遍历强引用的对象:

@objc fileprivate func app_viewDidAppear(_ animated: Bool) {
    self.app_viewDidAppear(animated)

    self.monitorAllRetainVariable(level: 0)
}

然后在 monitorAllRetainVariable 方法内部通过runtime提供给我们的 class_copyPropertyList 方法获取所有的属性,过滤出来强引用的属性即可:

```swift

private func getAllVariableName(cls:AnyClass) -> [String] {

let count = UnsafeMutablePointer<UInt32>.allocate(capacity: 0)
let properties = class_copyPropertyList(cls, count)

if Int(count[0]) == 0 {
    free(properties)
    return [String]()
}

var result = [String]()
for i in 0..<Int(count[0]) {

    guard let property = properties?[i] else {
        continue
    }
    let variable = Variable(property: property)
    guard let type = variable.type() else {
        continue
    }
    if type == self.classForCoder {
        continue
    }
    if variable.isStrong() == false {
        continue

分享给小伙伴们:
本文标签: 内存泄露

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号