在iOS中,内存泄漏是个很严重的问题,他会造成我们的程序在使用过程中本该释放的内存没有得到应有的释放,从而app占用内存不断变大,甚至出现闪退等严重问题。平常我们都会用 Instrument 的 Leaks或其他一些开源库进行内存泄露的排查,但它们都存在各种问题和不便,我们逐个来看这些工具的使用和存在的问题。
1.Leaks
先看看 Leaks,从苹果的开发者文档里可以看到,一个 app 的内存分三类:
- Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
- Abandoned memory: Memory still referenced by your application that has no useful purpose
- Cached memory: Memory still referenced by your application that might be used again for better performance.
其中 Leaked memory 和 Abandoned memory 都属于应该释放而没释放的内存,都是内存泄露,而 Leaks 工具只负责检测 Leaked memory,而不管 Abandoned memory。在 MRC 时代 Leaked memory 很常见,因为很容易忘了调用 release,但在 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露,应用有限。
1.1 通过xcode运行我们的项目
先通过xcode运行起来我们的项目,但是不要关闭(这点和Allocations内存检测不太一样,关于Allocations可以看我之前写的这篇文章),然后打开instruments,找到leaks,选择真机或者模拟器,找到对应的项目,运行起来。
在运行过程中如果发现Leak Checks(如上图)出现红色X说明检测到内存泄露,将鼠标点击Leak Checks,在下方即可看到内存泄漏的相关信息
1.2 定位内存泄漏代码
首先选择Call Tree,然后选择底部的Call Tree在弹窗中选择Invert Call Tree 和 Hide System Libraries,即可显示出具体内存泄漏的代码
如果上面还没出现具体代码,那么点击项目工程文件Buidl Setting搜索Debug Information Format-Debug里选择DWARF with dSYM File,这个和Allocations那个一样(关于Allocations可以看我之前写的这篇文章)
然而,目前的Leaks并不能解决我们常见开发中的内存泄漏问题,比如循环引用导致的页面无法释放:诸如block循环引用,代理强引用等。
2. MLeaksFinder
MLeaksFinder是腾讯开源的一个检测内存泄漏的库,提供了内存泄露检测更好的解决方案。只需要引入 MLeaksFinder,就可以自动在 App 运行过程检测到内存泄露的对象并立即提醒,无需打开额外的工具,也无需为了检测内存泄露而一个个场景去重复地操作。MLeaksFinder 目前能自动检测 UIViewController 和 UIView 对象的内存泄露,而且也可以扩展以检测其它类型的对象。
MLeaksFinder 的使用很简单,参照 https://github.com/Tencent/MLeaksFinder,通过pod将MLeaksFinder导入到项目里,就可以在运行时(debug 模式下)帮助你检测项目里的内存泄露了,无需修改任何业务逻辑代码,而且只在 debug 下开启,完全不影响你的 release 包。
当有内存泄漏的时候,会弹出提示框,提示你相关的信息或内存泄漏的页面。
2.1 原理
- 通过运行时 hook 系统的
viewdidDisappear
等页面消失的方法,在 hook 的方法里面添加willDealloc()
方法,各个子类自己实现willDealloc()
方法。 - NSObject的
willDealloc()
方法会有一个延迟执行 2s 的 alert 弹框,如果 2s 以后对象被释放,系统会把对象指针设置为nil,2s 以后也就不会有弹框出现,所以根据 2s 以后有没有弹框来判断对象有没有正确的释放。 - 最后会有一个 proxy 实例
objc_setAssociatedObject
在 object 上,如果上述弹窗提示未被释放的对象最后又释放了,则会调用 proxy 实例的dealloc
方法,然后弹窗提示用户对象最终还是释放了,避免了错误的判断。
2.2 说明
pod下来MLeaksFinder库,会发现里面还导入了一个FBRetainCycleDetector库,这是Facebook开源的一个库,是用来检测循环引用的,当传入内存中的任意一个 OC 对象(swift不行。。下面会说),FBRetainCycleDetector 会递归遍历该对象的所有强引用的对象,以检测以该对象为根结点的强引用树有没有循环引用。
然而,FBRetainCycleDetector 的使用存在两个问题:
- 需要找到候选的检测对象
- 检测循环引用比较耗时
正是由于这两个问题,FBRetainCycleDetector 通常是结合其它工具一起使用,通过其它工具先找出候选的检测对象,然后进行有选择的检测。当 MLeaksFinder 与 FBRetainCycleDetector 结合使用时,正好能达到很好的效果。我们先通过 MLeaksFinder 找到内存泄漏的对象,然后再过 FBRetainCycleDetector 检测该对象有没有循环引用即可。
2.2 实战
2.2.1 Swift
1 | import UIKit |
从别的界面push进入LeaksViewController
界面,这个界面中引用了自定义按钮里的闭包,并且在闭包里又使用了当前界面本身self
,从而造成循环引用。正常情况下,我们pop回上个页面,发现deinit方法是不走的,这时候用Leaks来检测,会发现清一色绿色,检测不出来内存泄漏问题。。(尴尬啊) 。
导入MLeaksFinder库后,再重新push一次,之后pop回上一个界面,2s后,会弹框如下:
在这里提示了我们LeaksViewController
这个界面有内存泄漏,点击Retain Cycle获取详细信息,发现获取失败,没有具体提示信息,这也对应上面说的FBRetainCycleDetector检测OC对象,所以我们需要自己去这个界面查找了。
2.2.2 0bjective-C
1 | #import "LeakViewController.h" |
1 | #import <UIKit/UIKit.h> //YellowBtn.h |
这里实现逻辑和上面swift的实现逻辑一致,导入MLeaksFinder库后,再重新push一次,之后pop回上一个界面,2s后,会弹框如下:
在这里提示了我们LeaksViewController
这个界面有内存泄漏,点击Retain Cycle获取详细信息,如下图
可以很精准的高速我们是LeaksViewController
界面下的_TwoBtn
里的clickBlock
引起的。
总结
在ARC的时代,instruments中的Leaks可使用的功能已经非常低了,而且使用起来很卡,体验实在差,我们可以用腾讯开源的MLeaksFinder
库配合Facebook的开源库FBRetainCycleDetector
来检测内存泄漏,不过可惜的是这个库已经两年没有维护了,虽然对于OC依然还是那么好用,但对于转入Swift的我们,就没那么精准,但仍然还是可以提供内存泄漏的界面,看需要吧,还是有点作用的。