背景
平时,我们常常会依靠Instrument的Leaks、Allocations以及其他一些开源库来予以内存泄露的排查,然而,它们全都有着各种各样的问题以及不便之处,我们逐一来瞧这些工具的使用情况以及所存在的问题。
Leaks
瞅一眼Leaks,于苹果的开发者文档之中能够瞧见,一款app的内存划分成三类:
其中,Leaked memory属于那种应该被释放但却没被释放的内存,Abandoned memory同样属于此类,它们都属于内存泄露这种情况之中,然而,Leaks工具仅仅负责对Leaked memory展开检测,对于Abandoned memory却并不予以理会。于MRC时期,Leaked memory颇为常见,缘由在于极易忘却调用release,然而在ARC时代,更为常见的内存泄露乃是由循环引用致使的Abandoned memory,Leaks工具无法查出此类内存泄露,其应用存有局限。
Allocations
对于被遗弃的记忆,可以通过仪器的分配情况检测出来。检测的方法是采用标记生成的方式,当你每一次点击标记生成的时候,分配情况会生成当前应用程序的内存快照,并且分配情况会记录从上次内存快照到此次内存快照的这个时间段之内,新分配的内存信息。举一个最为简单的例子:
我们能够持续反复地进行 push 和 pop 同一个 UIViewController 的操作,按理论讲,在实施 push 操作之前以及 pop 操作之后,app 会回归到相同的状态。所以,在 push 进程中新创建分配的内存,于 pop 之后理应被 dealloc 掉,除开前几次 push 或许存在着预热数据以及 cache 数据的情形之外。要是在经过数次 push 以及 pop 之后,内存依旧持续不断地增长,那么就存在内存泄露的状况。因为如此,所以我们于每次进行push之前以及pop之后,均会对Generation加以Mark,借由这般方式来观察内存是否会呈现无限制增长的情况。此方法于WWDC的视频那里能够见着:乃是Session 311 - Advanced Memory Analysis with Instruments,并且在苹果的开发者文档当中也有介绍:Finding Abandoned Memory里。
用这种方法来发现内存泄露还是很不方便的:
开源库
于GitHub之上,存在着相关项目,这些项目是围绕内存泄露检测的,像HeapInspector-for-iOS以及MSLeakHunter。
HeapInspector-for-iOS 能够算得上是 Allocations 的改良,它借着 hook 掉 alloc、dealloc、retain、release 这类方法,进而去记录对象的生命周期。而具体用来检测内存泄露的方法以及原理,与 Instrument 的 Allocations 是一模一样的。可是它跟 Allocations 相同的是,所存在的问题在于,你得一个一个场景地去重复进行操作,并且检测还不及时。
MSLeakHunter要简单不少,它专门针对UIViewController以及UIView展开检测,借助hook掉UIViewController的-viewDidDisappear:方法,并且认定在-viewDidDisappear:而后,UIViewController会迅速面临被释放的情况,要是UIViewController并未被成功释放,那就打出一则建议日志。这种做法事实上并非相当不错,-viewDidDisappear:这般被调用,或许是由于又推送进来一个全新的ViewController之时,将当前的ViewController给遮挡住了所以呢或许呈现出诸多错误的建议,需要依据你自身实际的操作去具体地审视日志。
MLeaksFinder
有一种具备更好内存泄露检测解决方案且名为 MLeaksfinder 的工具,只要将其引入,就能在 App 运转过程自动监测及即刻提示内存泄露对象,既不用开启以外的其他工具,也不用为测定这样的泄露而针对一个个情境去一回回反复地操作,它当前能够自动检查 UIViewController 以及 UIView 对象相关的内存泄露,同时亦可拓展来探寻其他类别对象的此类状况。
MLeaksFinder 的运用十分简便,参照 https://github.com/Zepo/MLeaksFinder。基本上而言,是将 MLeaksFinder 目录当中的文件添加至你的项目里。如此一来,便能够在运行的时候也就是 debug 模式下,助力你检测项目中的内存泄露情况。并且无需对任何业务逻辑代码作出修改,而且仅仅在 debug 状态下开启,完全不会对 release 包造成影响。
一旦出现内存泄露的情况,MLeaksFinder就会触发断言,并且能够精准无误地告知你究竟是哪个对象发生了泄露。在此处将其设计成触发断言而非记录日志以使程序仍旧运行,原因在于存在许多人不会去查看日志,触发断言则可迫使开发者留意到并着手进行修改,而不是拖延下去。
一旦中断言,控制台就会出现这样的提示,也就是查看View - UIViewController栈从顶部往下方去看时,能够发现那个显示该栈会告知你,有关MyTableViewController的UITableView的子视图当中的UITableViewWrapperView的子视图MyTableViewCell没有被释放。并且,在此处我们能够确定的是,MyTableViewController,UITableView,UITableViewWrapperView这三者已然成功地被释放掉了。
12345678
*** 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 有着如下这般的优点着哪:
原理
MLeaksFinder 最初是从 UIViewController 着手的,我们都清楚,当一个 UIViewController 被执行 pop 操作或者 dismiss 操作之后,这个 UIViewController 连同它的 view,view 的 subviews 等等将会迅速被释放(除非你把它设计成单例情形,又或者持有它具有很强引用关系,但通常是很少会如此去做的)。由此,我们仅需于一个 ViewController 被 pop 或者 dismiss 一小会儿之后,瞧瞧那个 UIViewController,它的 view,view 的 subviews 等之类的是否尚在。
具体的办法是,给基类NSObject增添一个方法,名为-willDealloc方法,此方法的功能是,先借助一个弱指针指向self,接着在一小段时长(3秒)之后,经由这个弱指针去调用-assertNotDealloc,而-assertNotDealloc的主要作用是直接进行断言。
12345678910
- (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, @“”);}

如此这般,当我们断定某个对象理应被释放之际,在释放之前调用此方法,若是三秒之后它被成功释放,weakSelf便指向nil,不会调入 -assertNotDealloc 方法,进而不会触发断言,要是它未被释放(出现泄露情况),-assertNotDealloc便会被调用致使断言触发。如此一来,当一个 UIViewController 被执行pop操作,又或者被perform dismiss动作时,我们秉持着它理应处于即将被释放的状态这一观点,我们会对该 UIViewController 之上所存在的全部 view 展开遍历,依照顺序去调用 -willDealloc 方法,倘若在历经3秒之后其并未成功被释放,那么便会触发断言。
在这里,有几个问题需要解决:
不入侵开发代码
这里运用了 AOP 技术,针对 UIViewController 和 UINavigationController 的 pop 以及 dismiss 方法进行了 hook 操作,至于怎样进行 hook,需参考 Method Swizzling。
遍历相关对象
在实际的项目当中,我们察觉到,有的时候,一个UIViewController被释放掉了,然而它的view却没有被释放,又或者,一个UIView被进行资源回收的时候,它的某个subview却没有被释放。这样的内存出现泄露的状况是十分常见的,所以,我们有必要去遍历基于UIViewController的整棵View - ViewController树。我们借助UIViewController的presentedViewController以及view属性,UIView的subviews属性等展开递归遍历,对于某些ViewController,像是UINavigationController,UISplitViewController等,我们还得遍历viewControllers属性。
构建堆栈信息
得构建View - ViewController栈信息,以此告知开发者是哪一个对象未被释放,于递归遍历View - ViewController树的时候,子节点的栈信息由父节点的栈信息再加上子结点信息而成。
例外机制
有一些ViewController,当它被pop或者dismiss后,是不会被释放的,比如说单例这种情况,所以要提供一种机制,让开发者能够指定哪个对象不会被释放,在此可以通过重载上面提到的 -willDealloc方法,直接返回NO就可以了。
特殊情况
出现某些特殊情形时,释放的时机存在差异,像系统进行手势返回之际,划至一半时保持按住,纵使已被弹出,然而此刻仍不会遭释放。ViewController 得等到彻底消失之后才予以释放,面对此间种种,需要开展特殊处置,而具体的特殊处置要依据具体情形来确定。
系统View
有一些系统当中的属于私有的View,是不会被实行释放操作的,这有可能是系统出现了bug,又或者是系统基于某些缘由特意如此去做的,在这里就不对其进行深入地探究了,所以是需要去建立起白名单的。
手动扩展
目前,MLeaksFinder仅仅检测ViewController以及View对象这两者。针对此情况,MLeaksFinder给出了一种手动扩展的机制,该机制在你从UIViewController出发以及从UIView出发时,能够实现检测其他类型对象之内存泄露。像下面所展示的那样,我们能够检测处于UIViewController底下的ViewModel:
1234567
- (BOOL)willDealloc {if (![super willDealloc]) {return NO;}MLCheck(self.viewModel);return YES;}
这儿的原理同上面的是一样的,宏MLCheck()所做之事是,为传进来的对象构建View-ViewController stack信息,且针对传进来的对象调用-willDealloc方法。
未来
MLeaksFinder处于起步阶段,它内存泄露检测的想法简单且直接。目前它只能自动检测与UIViewController及UIView相关的对象,然而在几个大项目中,它发挥了很大作用,帮助发现诸多历史存在的内存泄露,还确保新提交的UI相关代码不会引发新问题。MLeaksFinder 会持续去探寻、摸索那覆盖范围更为宽泛的情形,进而付诸成效地给出更为完整、周全、毫无遗缺的检验,其中涵盖那网络层面,还有数据存储层面如此之类等。