前言最近一直在研究如何在iOS应用中进行一些简单的内存监控,主要包括内存泄漏和内存使用情况。在开始记录自己的旅程之前,推荐一篇文章:从OOM到iOS内存管理|创客训练营。文章比较全面的介绍了iOS内存的基础知识。本文主要介绍如何调试内存泄漏、内存泄漏的代码检测、内存使用情况的获取等基础内容。篇幅较长,可按标题选择性阅读。内存泄漏工具检测内存泄漏的检测主要是在debug阶段。Xcode还提供了一些检测内存使用和内存泄漏的工具。毫无疑问,循环引用是造成内存泄漏的主要原因。下面是一个模拟循环引用的简单演示。classServer:NSObject{varclients:[Client]=[]funcadd(client:Client){self.clients.append(client)}}classClient:NSObject{varserver:Server?overrideinit(){super.init()}}classViewController2:UIViewController{letclient:Clientletserver:Serverinit(){self.client=Client.init()self.server=Server.init()super.init(nibName:nil,bundle:nil)self.client.server=serverself.server.add(client:client)}}通过代码可以推断,Server和Client对象的实例会造成循环引用,最终导致内存泄漏。下面介绍Xcode自带的两款内存泄漏检测工具。(ps:使用的Xcode版本是12.0)Instruments的AllocationsandLeaks可以检测app运行过程中的内存使用情况和内存泄漏情况,这也是笔者在日常开发中最常用的检测内存的方式。下面是使用Leaks检测到的内存泄漏的截图:在这里可以清楚的看到client和server对象都没有释放,形成了循环引用。(在研究过程中,发现可能存在一些Leaks检测不到的漏洞,这是我没有深究的缘故,有知道的朋友可以在下方留言讨论。)MemoryGraph是一个查看app内存使用情况的函数,可以清晰的查看对象的引用链。单击图中的按钮可在应用程序运行时打开MemoryGraph。接下来按照上面的demo检测的话就可以看到效果了。从上图可以看出,应该释放的client和server这两个对象由于循环引用导致内存泄漏。一般如果是漏物,后面会有紫色标记。ps:比较尴尬的是,当笔者在自己的iPhone7上运行公司的项目打不开时,会出现如图提示。目前还没有找到合适的解决方案:另一种方法是通过exporting.memgraphoccupancy在命令行查看一些内存占用情况。这里简单介绍一下使用leaks命令检查内存泄漏,一些高级方法可以参考这篇文章:Let'sdebugiOSmemory-MemoryGraphExport.memgraphfile:在上述模式下点击File->ExportMemoryGraph终端使用leaks命令查看内存泄漏分析。比如发生循环引用时,会打印出相关的引用:也可以打开malloc日志栈,获取根节点的backtrace。内存泄漏代码检测上面提到的检测方法都是借助Xcode工具实现的,对PC端的依赖比较大。有没有纯代码的检测方法?因为作者的目的是在发布环境中检测泄漏。而如果代码能够自动检测出来,在日常的开发调试阶段就可以自动发现一些内存泄漏。答案是肯定的。接下来介绍一下笔者在查阅资料的过程中所研究的三个开源库。通过阅读开源库的源码和一些文章,作者得出了一个公式,后续的内存泄漏代码检测也可以通过这个公式来总结。这里我把内存泄漏的检测总结为两个方面:触发时机和验证方法。iOS内存泄漏代码检测有一定的局限性。对比一些开源项目的实现,需要找到一个合理的触发时机(比如MLeaksFinder会以hookViewController的生命周期作为检测起点,后面会讲到)。这里转载的一个简单的解决方案是使用MethodSwizzling的方法hookvc方法进行触发验证。iOS自定义内存监控结合上面的例子套用公式:触发时机:用MethodSwizzling代替vc的dismiss和viewWillDisappear,间接判断vc的销毁。这会触发检测。验证方法:延时2s后调用vc本身的一个扩展实例方法。测试结果:正常情况下,2s后,对象会在vc销毁后释放,所以不能调用自己的实例方法。如果调用成功,则对象已经泄露。以上简单思路基本可以贯穿大部分内存泄漏的代码检测实现,后面介绍的开源库也大同小异。RIBs-LeakDetectorRIBs是uber开源的一个app架构设计框架。作者在库中找到了一个检测内存泄漏的工具LeakDetector.swift。巧妙的是这里使用了NSMapTable.strongToWeakObjects()的一个对象trackingObjects来存放需要观察的对象。目的。文档的解释是key值是强引用,value值是弱引用。不友好的是,这个库的触发时机需要开发者自行寻找。比如在vc的deinit上,可以监控viewModel是否有内存泄漏:deinit{LeakDetector.instance.expectDeallocate(object:viewModel)}应用公式:触发时机:开发者需要找到触发时机自己实现,比如vc中的deinit验证方式:预先将要观察的对象添加到NSMapTable中,延时指定时间后根据对应的key获取value检测结果:如果value为空,则表示内存已经释放(弱引用),否则会发生内存泄漏。优点:值得学习,1.借鉴了NSMapTable的特性;2、定时部分的逻辑使用RxSwift实现,有利于检测事件的取消和代码的解耦。缺点:1、依赖RxSwift等大型第三方库,不够轻量;2.需要自己找触发时序,逻辑上可能兼容性不好。LifetimeTrackerLifetimeTracker这是一个比较有趣的库。检查是否泄漏的依据是开发者设置的最大对象数。下面是拦截它的readme的用法:LifetimeTrackable协议中有一个LifetimeConfiguration类对象来完成maxCount的配置,在合适的时候,比如init方法调用trackLifetime触发leakcheck。原理就是里面有一个集合来存放每个对象的label(这里是把对象的信息生成一个模型),然后每次触发trackLifetime的时候都会校验这个数字。具体的源码实现可以在LifetimeTracker.swift中找到。ps:值得注意的是每次trackLifetime的次数+1,还有一次onDealloc机会-1。这里我们使用associatedattribute(如上图所示)预先将一个对象与被观察对象关联起来。如果被观察对象调用deinit,关联属性对象的deinit也会被调用。这样就可以间接判断被观察对象的释放情况。关于关联属性的介绍,请看这篇文章objc_setAssociatedObject关联详解。套用公式:触发时机:开发者需要在对象初始化时调用trackLifetime。验证方法:trackLifetime时,会添加一个被观察对象的计数,并关联到一个对象。当关联对象被deinit调用时,计数将自动为-1。检测结果:如果A计数大于LifetimeConfiguration的maxCount则为leak。ps:本库也有组图。我的理解是有些类型可以组合使用,分组可以更好的管理。这里就不做过多介绍了。优点:1、对vc等视图的依赖性更强;2、根据objc_setAssociatedObject,被动检测释放对象,不耽误主动检测。缺点:1、需要针对使用场景预先设置maxCount的限制;2.leakcheck需要调用trackLifetime来触发,触发时机比较晚。MLeaksFinderMLeaksFinder是腾讯开源的内存泄漏检测库。原理和本节开头提供的例子大致相似:将vc的生命周期换成MethodSwizzling,然后触发check。它在加载对象时自动替换,从而可以实现对视图级内存泄漏的非侵入式监控。接下来我们看一下整个库的灵魂,也就是触发检查的方法:上面是库中ViewController的扩展,灵魂是willDealloc方法。这里,willDealloc方法会在触发vc的dismiss时触发。方法中会把vc的sub-view和sub-vc加入到观察中,从而最大程度的观察到view对象的泄漏。让我们看一下willReleaseChildren和willReleaseChild方法的作用。以上是库中NSObject的扩展。实际上,objc_setAssociatedObject关联了一个对象的地址集合(parentPtrs)和对象的引用栈(viewStack)。参考堆栈。最后还调用了子对象的willDealloc。ps:willDealloc的实现也是之前的老方法,延迟2s调用对象的方法,从而判断是否泄漏。不幸的是,如果你想观察自定义类型的属性,你仍然需要手动触发它,比如观察vc的viewModel是否存在泄漏:@objcdynamicpublicoverridefuncwillDealloc()->Bool{if!super.willDealloc(){returnfalse}self.willReleaseChildren([self.viewModel])returntrue}套用公式:触发时机:从vc销毁开始逐层调用willDealloc。验证方法:延时2s后调用NSObject扩展方法assertNotDealloc。测试结果:一般是调用成功,大概率是漏电了。优点:1.无需侵入式观察视图级对象;2、找触发时机的兼容逻辑比较完善;3.记录的引用栈比较完整,方便定位问题。缺点:1、非视图对象需要手工处理;2、依赖NSObject,对swift上非继承的NSObject类型不友好;3.观察方法中的对象等非全局属性,可能很难找到触发时机。内存泄漏代码检测总结综上所述,几个开源库的设计和分析其实和作者在本节开头提出的公式非常吻合。遗憾的是,它们或多或少都对代码有侵入性,而进程上(方法内部)的对象观察对代码的侵入性更大,不友好。综合以上分析的优缺点,笔者最终选择了MLeaksFinder,并将花少量时间介绍笔者对该库的小改动。MLeaksFinder内存泄漏记录代码分析如上图所示,一旦MLeaksFinder检测到内存泄漏:首先,它会为泄漏的对象关联一个MLeakedObjectProxy对象。主要作用是观察MLeakedObjectProxy对象的释放一旦泄漏对象的生命周期被释放。这种方法类似于上面提到的LifetimeTracker。信息泄露发布时会有弹窗提示开发者进行响应。结合上面的分析,如果想在泄露后做一些自定义的记录(比如开发日志记录等),可以在弹窗中添加或者修改自己的逻辑。这部分实现在DoraemonKit中的DoraemonKit-MLeaksFinder中得到了很好的体现。内存使用率的应用性能检测包括当前内存使用率的获取。这里分享两篇文章:从OOM到iOS内存管理|造物主训练营,iOS开发--APP性能检测解决方案总结(一).介绍的比较详细,这里就不赘述了。下面是通过资料总结的一些方法,有需要的可以借鉴。获取当前app占用内存量#include
