一、背景

  • 在编写日常业务代码时,或多或少都会引入一些导致内存泄漏的代码,而这种行为又很难被监控,这就导致应用内存泄漏的口子越开越大,直接影响到线上应用的稳定性。
  • 虽然 Xcode 的 Instrucment 提供了 Leaks 和 Allocations 工具能精准地定位内存泄漏问题,但是这种方式相对比较繁琐,需要开发人员频繁地去操作应用界面,以触发泄漏场景,所以 Leaks 和 Allocations 更加适合定期组织的大排查,作为监测手段,则显得笨重。
  • 对于内存泄漏的监测,业内已经有了两款成熟的开源工具,分别是 PLeakSniffer 和 MLeaksFinder。
    • PLeakSniffer 使用 Ping-Pong 方式监测对象是否存活,在进入页面时,创建控制器关联的一系列对象代理,根据这些代理在控制器销毁时能否响应 Ping 判断代理对应的对象是否泄漏。
    • MLeaksFinder 则是在控制器销毁时,延迟 3s 后再向监测对象发送消息,根据监测对象能否响应消息判断其是否泄漏。
  • PLeakSniffer 和 MLeaksFinder 这两个基本能覆盖大部分对象泄漏或者延迟释放的场景,考虑到性能损耗以及内存占用因素,个人更偏向于第二种方案。
  • 个人使用 MLeaksFinder,还存在以下问题:
    • 没有处理集合对象;
    • 没有处理对象持有的属性;
    • 每个对象都触发 3s 延迟机制,没有缓存后统一处理;
    • 检测结果输出分散。
  • PLeakSniffer 存在以下问题:
    • 没有处理集合对象;
    • 处理对象持有属性时,系统类过滤不全面;
    • 处理对象持有属性时,通过 KVC 访问属性导致一些懒加载的触发;
    • 无法处理未添加到视图栈中的泄漏视图;
    • 检测结果输出分散。
  • 对于检测到泄漏对象的交互处理,两者都提供了终端 log 输出和 alert 提示功能,MLeaksFinder 甚至可以直接通过断言中断应用,这种提示在开发阶段尚可接受,但是在提测阶段,强交互会给测试人员造成困扰。至于为什么在提测阶段还要集成泄漏监测工具,主要有两个原因:
    • 应用功能过多的情况下,开发人员无法兼顾到老页面,一些老页面的泄漏场景可以通过测试人员在测试时触发,收集之后再统一处理;
    • 在组件化开发环境下,开发人员可能并没有集成泄漏监测工具,这种情况下,需要在提测阶段统一收集没有解决的泄漏问题。
  • 因此,对于监测输出的诉求有两点:
    • 开发时,通过终端日志提示开发者出现了内存泄漏;
    • 提测时,收集内存泄漏的信息并上传至效能后台,统一分配处理;

二、监测入口

  • 和 MLeaksFinder 一样,选择延迟 3s 的机制来判断对象是否泄漏,但是实现的细节略有差别。首先,监测入口变更为 viewDidDisappear: 方法,只需在控制器被父控制器中移除或者被 Dismissed 时,触发监测动作即可:
   - (void)LeaksMonitor_viewDidDisappear:(BOOL)animated {[self LeaksMonitor_viewDidDisappear:animated];if (![self isMovingFromParentViewController] && ![self isBeingDismissed]) {return;}[[YDWLeaksMonitor shared] detectLeaksForObject:self];}
  • 在应用中,还有一种监测入口出现在变更根控制器时,由于直接设置根控制器不会触发 viewDidDisappear 方法,所以需要另外设置 :
   - (void)LeaksMonitor_setRootViewController:(UIViewController *)rootViewController {if (self.rootViewController && ![self.rootViewController isEqual:rootViewController]) {[[YDWLeaksMonitor shared] detectLeaksForObject:self.rootViewController];}[self LeaksMonitor_setRootViewController:rootViewController];}
  • 为了能够统一处理控制器及其持有对象,可以像 PLeakSniffer 一样,给每个对象包装一层代理 :
   @interface YDWLeakObjectProxy : NSObject// 持有 target 的对象弱引用@property (weak, nonatomic) id host;// 被 host 持有的对象弱引用@property (weak, nonatomic, readonly) id target;@end
  • 只要 host 释放了而 target 没释放,则视 target 已泄漏,如果 host 未释放,则不检测 target,然后使用一个 collector 去收集这些对象对应的 proxy ,在收集完之后统一监测 collector 中的所有 proxy ,这样就可以在一个控制器监测完成后,统一上传监测出的泄漏点 :
   - (void)detectLeaksForObject:(id <YDWLeakObjectProxyCollectable>)object {// 收集控制器关联的所有 proxy// 收集之后再统一处理,避免对每一个对象都进行 3s 检测YDWLeakObjectProxyCollector *collector = [[YDWLeakObjectProxyCollector alloc] init];YDWLeakContext *context = [[YDWLeakContext alloc] init];context.host = object;(void)[object LeaksMonitor_collectProxiesForCollector:collector withContext:context];// 检测 3s 之后,collector 中的所有 proxy 是否正常[self detectProxyCollector:collector];}

三、收集对象信息

  • 因为要对不同的类做特异化处理,因此先定义一个协议,通过这个协议中的 collect 方法去收集不同类实例化对象的 proxy :
   @protocol YDWLeakObjectProxyCollectable <NSObject>/**收集对象及其名下的所有成员变量对应的 proxy@param collector 收集器,存储 proxy@param ctx 上下文*/- (void)LeaksMonitor_collectProxiesForCollector:( YDWLeakObjectProxyCollector * _Nonnull )collector withContext:( YDWLeakContext * _Nullable )ctx;@end
  • 关键在于如何让 NSObject 实现此协议,主要有四个步骤 :
    • 过滤系统类调用;
    • 向 collector 添加封装的 proxy;
    • 循环遍历对象对应的非系统类 / 父类属性,找出 copy / strong 类型属性,并获取其对应的成员变量值;
    • 向收集的所有成员变量对象发送 collect 方法。
  • NSObject 实现 collect 协议方法后,其子类就可以通过这个方法递归地收集名下需要监测的属性信息。比如对于集合类型 NSArray ,实现协议方法如下,表示收集自身和每个集合元素的信息,不过由于 NSArray 是系统类,所以其实例化对象并不会被收集进 collector ,如果要收集系统类的属性信息,只能通过让系统类实现协议并重载 collect 方法,手动向属性值发送 collect 消息实现,UIViewController 的 childViewControllers、presentedViewController、view 属性也同理 :
   - (void)LeaksMonitor_collectProxiesForCollector:(YDWYDWLeakObjectProxyCollector *)collector withContext:(YDWLeakContext *)ctx {[super LeaksMonitor_collectProxiesForCollector:collector withContext:ctx];[self enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {if ([obj conformsToProtocol:@protocol(YDWLeakObjectProxyCollectable)]) {[obj LeaksMonitor_collectProxiesForCollector:collector withContext:LM_CTX_D(ctx, @"contains")];}}];}
  • 需要注意的是,直接调用属性的 getter 方法获取属性值,可能会触发属性懒加载,导致出现意料之外的问题 (比如调用 UIViewController 的 view 会触发 viewDidLoad),所以要通过 object_getIvar 去获取属性对应的成员变量值。当然,这种处理方式会导致无法收集某些没有对应成员变量值的属性,比如关联对象、控制器的 view 等属性,权衡利弊之后,可以选择忽略这种属性的监测。
  • 除了收集必要的对象信息之外,我还记录了监测对象的引用路径信息,也就是上面 LM_CTX_D 宏做的事情。有些情况下,对象的引用路径能帮助我们发现,路径上的哪些操作导致了对象的泄漏,特别是在网页上浏览泄漏信息时,如果只有泄漏对象类和引用泄漏对象类两个信息,脱离了对象泄漏时的上下文环境,会增加修复的难度。有了引用路径信息后,输出的泄漏信息如下 :
   [O : YDWViewController.view->UIView.subviews->__NSArrayM(contains)->A.subviews->__NSArrayM(contains)->OYDWViewController : YDWViewController.childViewControllers->YDWViewController__NSCFTimer : YDWViewController.timer->__NSCFTimer]

四、过滤系统类

  • 系统类信息并不是需要关心的,过滤掉并不会影响到最终的监测结果。目前我尝试了两种方式来确定一个类是否为系统类:
    • 通过类所在 NSBundle 的路径;
    • 通过类所在地址。
  • 第一种的逻辑较为简单,代码如下:
   BOOL LMIsSystemClass(Class cls) {NSBundle *bundle = [NSBundle bundleForClass:cls];if ([bundle isEqual:[NSBundle mainBundle]]) {return NO;}static NSString *embededDirPath;if (!embededDirPath) {embededDirPath = [[NSBundle mainBundle].bundleURL URLByAppendingPathComponent:@"Frameworks"].absoluteString;}return ![bundle.bundlePath hasPrefix:embededDirPath];}
  • 应用的主二进制文件,和开发者添加的 embeded frameworks 都会在固定的文件目录下,所以直接比对路径前缀即可。
  • 第二种方式的实现步骤如下:
    • 遍历所有的 image ,通过 image 的名称判断是否为系统 image;
    • 缓存所有系统 image 的起始位置,也就是 mach_header 的地址;
    • 判断类是否为系统类时,使用 dladdr 函数获取类所在 image 的信息,通过 dli_fbase 字段获取起始地址;
    • 比对 image 的起始地址得知是否为系统类。
  • 实际尝试下来后,发现第二种方式耗时会比第一种多,dladdr 函数占用了大部分时间(内部会遍历所有 image 的开始结束地址,和传入的地址进行比对),所以最终选择了第一种方式作为判断依据。
  • 过滤系统类时,针对那种会自泄漏的对象,需要进行特殊处理,不予过滤。比如 NSTimer / CADisplayLink 对象的常见内存泄漏场景,除了 target 强引用控制器造成循环引用域外,还有一种是打破了循环引用但没有在控制器销毁时执行 invalidate 操作,因为 NSTimer 由 RunLoop 持有,不手动停止的情况下,就会造成泄漏。

五、局限性

  • 基于延时的内存泄漏监测机制虽然适用于大部分视图、控制器和一般属性的泄漏场景,但是还有少部分情况,这种机制无法处理,比如单例对象和共享对象。
  • 首先说下单例对象,假设有 singleton 属性,其 getter 方法返回 Singleton 单例,这时延时监测机制无法自动过滤这种情况,依然会认为 singleton 泄漏了。有一种检测属性返回值是否为单例的方法,就是向返回值对应类发送 init 或者 share 相关方法,通过方法返回值和属性返回值的对比结果来判断,但是事实上我们无法确定业务方的单例是否重写了 init,也无法获知具体的单例类方法,所以这种方案适用面比较局限。单例对象的处理,目前还是通过白名单的方式处理较为稳妥。
  • 共享对象的应用场景就比较普遍了,比如现有 A,B 页面,A 页面持有模型 M ,在跳转至 B 页面时,会将 M 传递给 B ,B 强引用了 M ,当 B 销毁时, M 不会销毁,而 M 又是 B 某个属性的值,所以监测机制会判断 M 泄漏了,实际上 M 只是 A 传递给 B 的共享对象。在一个控制器做完检测就需要上传至效能后台的情况下,共享对象还没有很好的处理方法,后期考虑结合 FBRetainCycleDetector 查找泄漏对象的循环引用信息,然后一并上传至效能后台,方便排查这种情况。因为每次 pop 都使用 FBRetainCycleDetector 检测控制器会比较耗时、甚至会造成延迟释放和卡顿,所以先用延时机制找出潜在的泄漏对象,再使用 FBRetainCycleDetector 检测这些泄漏对象,能极大得减少需要处理的对象数量。最终网页呈现的效果如下:

六、总结

  • 像内存泄露这种问题,最好在应用初期就开始着手监测和解决,否则当应用功能代码逐渐增多后,回过头来处理这种问题费时费力,还是比较麻烦的。
  • 基于 PLeakSniffer 和 MLeaksFinder 监测工具的基础上,结合团队业务情况,进行了一些的改造,添加了集合对象的处理、引用路径的记录、对象的统一检测等功能,优化了部分有问题的代码,在一定程度上提升了延时机制的可用性。

iOS之深入定制基于PLeakSniffer和MLeaksFinder的内存泄漏检测工具相关推荐

  1. MLeaksFinder :腾讯开源的 iOS 内存泄漏检测工具

    一.工具简介 MLeaksFinder :腾讯开源的 iOS 内存泄漏检测工具 工具优势:在日常开发调试或测试业务逻辑过程中,可以自动发现并警告内存泄漏.暂时没有发现误报:基本上报了leak的  进去 ...

  2. 插桩valgrind_基于动态插桩的CC++内存泄漏检测工具的设计与实现.pdf

    基于动态插桩的CC++内存泄漏检测工具的设计与实现.pdf 第32卷第6期 计 算 机 应 用 研 究 V01.32No.6 20l5年 6月 ApplicationResearchofCompute ...

  3. iOS开发之内存泄漏检测工具-Leaks

    引言 我们在实际开发过程中,经常会不小心造成循环引用问题,从而造成内存泄漏问题,那么我们该如何检测我们工程那个位置存在内存泄漏问题呢?这就需要用到Xcode自带的内存泄漏检测工具-Leaks. 内存泄 ...

  4. 基于Android Studio的Android内存泄漏检测方法

    自从Google在2013年发布了Android Studio后,Android Studio凭借着自己良好的内存优化,酷炫的UI主题,强大的自动补全提示以及Gradle的编译支持正逐步取代Eclip ...

  5. 精准 iOS 内存泄露检测工具

    MLeaksFinder:精准 iOS 内存泄露检测工具 发表于 2016-02-22   |   zepo   |   23 Comments 背景 平常我们都会用 Instrument 的 Lea ...

  6. 基于Android Studio的内存泄漏检测与解决全攻略

    自从Google在2013年发布了Android Studio后,Android Studio凭借着自己良好的内存优化,酷炫的UI主题,强大的自动补全提示以及Gradle的编译支持正逐步取代Eclip ...

  7. C++中基于Crt的内存泄漏检测

    尽管这个概念已经让人说滥了 ,还是想简单记录一下, 以备以后查询. #ifdef _DEBUG #define DEBUG_CLIENTBLOCK   new( _CLIENT_BLOCK, __FI ...

  8. iOS 内存泄漏检测 Instruments Leaks

    Xcode 中 按住 command + I 或者菜单栏 Product – Profile 2. 双击 Leaks 或者按 choose,打开 Leaks 面板 3. 在显示的 Leaks 面板中, ...

  9. iOS循环引用问题集合、内存泄漏、僵尸对象、代码静态分析

    内存泄漏:https://my.oschina.net/llfk/blog/1031291 内存泄漏监测自动化:http://www.cocoachina.com/articles/18490 fac ...

最新文章

  1. 微生物组助手——最易学的扩增子、宏基因组分析流程
  2. sort降序shell_希爾排序(Shell Sort)
  3. 记一次golang中sync.Map并发创建、读取的问题
  4. 【路径规划】基于matlab A_star算法机器人动静态避障路径规划【含Matlab源码 371期】
  5. 施耐德变频器与昆仑通态触摸屏Modbus通讯程序实现正转反转,启停复位,频率设定等功能
  6. Android font-awesome 4.2 icons png(包含holo-light和holo-dark)
  7. 白话电视:被移动设备抢走的光环,靠什么夺回来?
  8. 东原罗韶颖:城市深耕中的社区商业逻辑
  9. Oracle的软解析(soft prase)和硬解析(hard prase)及绑定变量
  10. pythonstdin_理解Python中的stdin stdout stderr - The Hard Way Is Easier
  11. JDK 1.7 基本概念和目录结构
  12. 用Servlet实现统计网站被访问次数的功能
  13. 【JavaWeb】Servlet系列——响应HTML代码、Servlet连接数据库、IDEA开发Servlet程序、Servlet对象的生命周期、GenericServelet适配器模式
  14. Unity --- 角色移动时播放动画 与 动画剪辑
  15. 浏览器无法访问某个网站,其他网站都正常
  16. 基于UE4 的AirSim虚拟仿真
  17. Elasticsearch: Query string与Simple query string
  18. 未来五年,不懂人工智能的程序员会被淘汰吗?
  19. 大数据技术与应用专业
  20. Octave工具学习

热门文章

  1. 你不知道Linux的10个最危险的命令
  2. 鸟哥Linux私房菜(基础篇)——第五章:首次登入与在线求助 man page笔记
  3. 一个简单的案例带你入门Dubbo分布式框架
  4. Webpack单元测试,e2e测试
  5. 洛谷1231 教辅的组成
  6. 数据类型之字符串练习
  7. 防止QQ密码被盗的五个绝招
  8. 和鸿蒙系统合作品牌,华为:明年将有超40家主流品牌、1亿台设备成为鸿蒙系统新入口...
  9. c语言 sdk,适用于 C 语言的 Azure IoT 设备 SDK
  10. scss怎么引入到html,Sass 导入指令