☰
MLeaksFinder 实践与源码解析:在开发阶段自动定位 iOS 内存泄漏
2026/10/15 2:10:56 网站建设 项目流程
  • 开发工具
  • 质量保障

【免费下载链接】MLeaksFinder

Find memory leaks in your iOS app at develop time.

项目地址:https://gitcode.com/gh_mirrors/ml/MLeaksFinder
点击查看免费下载

MLeaksFinder 是腾讯开源的 iOS 内存泄漏检测工具,其核心思路是:在开发阶段(Debug 构建)自动监控 UIView 与 UIViewController 对象的生命周期,一旦发现"本该释放却仍存活"的对象,就弹出带完整 View-ViewController 调用栈的告警,并支持扩展检测任意业务对象、可选集成 FBRetainCycleDetector 定位循环引用。阅读本文后,你将掌握 MLeaksFinder 的接入方式、运行原理、屏蔽误报的方法,以及如何用MLCheck扩展检测自己的业务对象。

本文以仓库根目录的 README.md 为主线,并结合MLeaksFinder/目录下的全部源码实现与 FAQ-CN.md 进行深度展开。官方还维护了一份中文 FAQ,遇到接入问题时可对照排查。

一、MLeaksFinder 是什么

根据 README.md 的定位,MLeaksFinder 帮助你在开发阶段(develop time)发现 iOS App 中的内存泄漏:

  • 自动检测:无需人工埋点,自动发现 UIView 和 UIViewController 对象的泄漏;
  • 直观告警:泄漏发生时,弹出 Alert 并在其中展示泄漏对象所在的 View-ViewController 栈,一眼定位"谁没被释放";
  • 可扩展:除 UIView / UIViewController 外,开发者可以把检测范围扩展到任意类型的对象(见下文"扩展检测其他对象");
  • 循环引用辅助:可选用 Facebook 的 FBRetainCycleDetector 为泄漏对象尝试找出循环引用(retain cycle)。

需要强调的是,MLeaksFinder 的定位是"开发期工具"而非线上监控:它的默认行为只在 Debug 构建下生效(详见下文宏开关说明),通过牺牲少量运行时代价换取开发时的即时反馈。

二、快速接入:一行 pod 搞定

在项目的 Podfile 中加入:

pod 'MLeaksFinder'

然后执行pod install。根据 README.md 的说明,MLeaksFinder 在pod install之后即自动生效,不需要添加任何代码,也不需要 import 任何头文件。这是它区别于传统"手动调用检测 API"类工具的最大特点——底层通过 Category + Method Swizzling 在 App 启动时自动注入检测逻辑(详见第三节)。

从仓库的 MLeaksFinder.podspec 可以确认以下事实:

Podspec 字段值说明
s.nameMLeaksFinder组件名
s.version1.0.0当前仓库版本号
s.ios.deployment_target6.0最低支持 iOS 6.0
s.source_filesMLeaksFinder/**/*打包目录为仓库内MLeaksFinder/全部源文件
s.public_header_filesMLeaksFinder.h、NSObject+MemoryLeak.h对外暴露的公共头文件

生效范围:默认只在 Debug 生效

在 MLeaksFinder.h 中可以看到生效开关的默认行为:

//#define MEMORY_LEAKS_FINDER_ENABLED 0 #ifdef MEMORY_LEAKS_FINDER_ENABLED #define _INTERNAL_MLF_ENABLED MEMORY_LEAKS_FINDER_ENABLED #else #define _INTERNAL_MLF_ENABLED DEBUG #endif

即:默认情况下,_INTERNAL_MLF_ENABLED直接等于DEBUG宏——Debug 构建自动开启检测,Release 构建自动关闭,无需任何额外配置;如果你需要手动控制,可以取消MEMORY_LEAKS_FINDER_ENABLED的注释并显式指定 0 或 1。所有扩展类(如UIViewController+MemoryLeak.m)的实现都包裹在#if _INTERNAL_MLF_ENABLED中,关闭后相关代码整体不会参与编译。

如何验证接入成功

根据 FAQ-CN.md 的排查建议:

  1. 在 UIViewController+MemoryLeak.m 的+ (void)load方法里加一个断点,App 启动时若能进入该方法,说明 Category 注入成功,否则说明接入失败;
  2. 使用 CocoaPods 安装时注意查看是否有 warnings,特别是OTHER_LDFLAGS相关的 warning;若有,在主工程 Build Settings → Other Linker Flags 中加上-ObjC。

提示:+ (void)load会在类首次加载时执行,MLeaksFinder 正是利用它完成一系列 Method Swizzling 注入(详见第四节)。

三、核心机制:willDealloc 与"延迟二次确认"

理解 MLeaksFinder 的原理,只需要抓住一个关键设计:它并不实时跟踪对象,而是对"应该被释放"的对象做一个延迟复查。

3.1 统一的检测入口willDealloc

所有检测最终都收敛到NSObject分类里的- (BOOL)willDealloc方法(见 NSObject+MemoryLeak.m)。其执行逻辑如下:

- (BOOL)willDealloc { NSString *className = NSStringFromClass([self class]); if ([[NSObject classNamesWhitelist] containsObject:className]) return NO; NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey); if ([senderPtr isEqualToNumber:@((uintptr_t)self)]) return NO; __weak id weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __strong id strongSelf = weakSelf; [strongSelf assertNotDealloc]; }); return YES; }

该方法按顺序做三件事:

  1. 白名单检查:若对象所属类在白名单内,直接返回NO(不触发检测),用于规避系统类的已知误报;
  2. 最新触摸对象检查:若对象是最近一次用户触摸的 View(kLatestSenderKey记录的指针),也返回NO,避免在触摸事件处理期间误判(详见第四节);
  3. 延迟二次确认:对self取一个__weak弱引用,然后在主队列上dispatch_after2 秒,检查弱引用是否还能取出强对象——这就是 MLeaksFinder 判定的关键:
    • 如果对象在 2 秒内正常释放,weakSelf为 nil,assertNotDealloc不会执行,一切安静;
    • 如果 2 秒后对象依然存活,说明它"应该释放而没释放",触发assertNotDealloc上报泄漏。

这段逻辑对应的正是"内存泄漏 = 本该释放却仍存活"的定义。而"本该释放"的时机,则由各 UIKit 扩展类在合适的生命周期钩子(pop、disappear、dismiss 等)处调用willDealloc来定义(见第四节)。

3.2 上报与去重:MLeakedObjectProxy

一旦确认疑似泄漏,assertNotDealloc(见 NSObject+MemoryLeak.m)会做两件事:

  1. 通过[MLeakedObjectProxy isAnyObjectLeakedAtPtrs:]检查该对象是否已上报过(内部维护一个NSMutableSet *leakedObjectPtrs指针集合),避免同一对象重复弹窗;
  2. 调用[MLeakedObjectProxy addLeakedObject:self]登记泄漏对象,同时输出一条包含类名和 View-ViewController 栈的NSLog。

MLeakedObjectProxy.m 中的addLeakedObject:在登记对象指针后,通过 MLeaksMessenger.m 弹出标题为 "Memory Leak" 的 UIAlertView,消息内容即该对象的viewStack。这里还有一个贴心设计:代理对象以objc_setAssociatedObject关联在泄漏对象上,一旦泄漏对象日后真的被释放,MLeakedObjectProxy的dealloc会再弹一条 "Object Deallocated" 的提示,告诉你"那个疑似泄漏的对象后来被释放了",方便反向印证是否为误报。

3.3 View-ViewController 栈是如何生成的

viewStack是贯穿整个机制的"坐标系统"。每个对象通过 Associated Object 挂载了:

  • kViewStackKey:从 ViewController 到该对象的路径描述数组;
  • kParentPtrsKey:父链路所有对象的指针集合,用于去重判断。

当检测一个对象时,其子对象会在willReleaseObject:relationship:/willReleaseChild:/willReleaseChildren:中被逐级挂上栈(见 NSObject+MemoryLeak.m)。于是最终呈现的告警就是一条完整的"从 ViewController 出发的视图链"。

四、自动检测的对象范围与注入点

MLeaksFinder 默认覆盖 UIViewController、UIView 及常见的容器控制器,注入点全部通过+ (void)load+swizzleSEL:withSEL:(定义在 NSObject+MemoryLeak.m)完成。各扩展类的职责如下表:

扩展类注入点(swizzle 的方法)检测内容
UIViewController+MemoryLeak.mviewDidDisappear:、viewWillAppear:、dismissViewControllerAnimated:completion:控制器自身 +childViewControllers+presentedViewController+view
UIView+MemoryLeak.m无需 swizzle,被 VC 触发subviews递归
UINavigationController+MemoryLeak.mpushViewController:animated:、popViewControllerAnimated:、popToViewController:animated:、popToRootViewControllerAnimated:viewControllers
UIPageViewController+MemoryLeak.m无viewControllers
UISplitViewController+MemoryLeak.m无viewControllers
UITabBarController+MemoryLeak.m无viewControllers

4.1 UIViewController:三个关键钩子

以 UIViewController+MemoryLeak.m 为例,它 swizzle 了三个方法:

  • viewWillAppear::出现时把关联对象kHasBeenPoppedKey置为NO,标记"当前控制器还在栈上";
  • viewDidDisappear::若kHasBeenPoppedKey为YES(即已被 pop),则触发willDealloc;
  • dismissViewControllerAnimated:completion::dismiss 后对presentedViewController(或自身,当它作为被 present 的一方)触发willDealloc。

而- (BOOL)willDealloc(见 UIViewController+MemoryLeak.m)在调用[super willDealloc]之后,会继续向下释放childViewControllers、presentedViewController,以及已加载的view——把检测递归扩散到整棵视图树。

4.2 UINavigationController:处理 pop 的时序细节

UINavigationController+MemoryLeak.m 是处理细节最多的扩展类:

  • popViewControllerAnimated:返回的控制器并不会立即释放(尤其左边缘滑动返回时),所以它把kHasBeenPoppedKey置为YES,交给viewDidDisappear:阶段再触发检测;
  • popToViewController:/popToRootViewControllerAnimated:返回的控制器数组直接逐个触发willDealloc;
  • 针对UISplitViewController的 Master-Detail 场景做了特殊处理:Detail 控制器 pop 后不会立即释放,而是暂存到kPoppedDetailVCKey,等下一次 push 新 Detail 时再触发其检测。

4.3 防误报:UIApplication 与 UITouch 的"最新触摸对象"

两个看似不相关的扩展类实际服务于误报过滤:

  • UIApplication+MemoryLeak.m swizzle 了sendAction:to:from:forEvent:,每次事件分发时把sender指针记录为kLatestSenderKey;
  • UITouch+MemoryLeak.m swizzle 了setView:,把触摸命中的 View 指针也记录为kLatestSenderKey。

回到第三节的willDealloc:如果待检测对象恰好是"最近被触摸/最近参与事件"的对象,则跳过检测(返回NO)。这是为了避免"对象因为正处于触摸响应链中被临时持有"而产生的误报。

五、阅读泄漏报告:View-ViewController 栈

README 给出了一个典型告警示例:

Memory Leak ( MyTableViewController, UITableView, UITableViewWrapperView, MyTableViewCell )

这个栈的阅读规则是:从栈顶开始,前面的对象都已被确认成功释放,栈底最后出现的那个对象就是疑似泄漏对象。在上例中,MyTableViewController、UITableView、UITableViewWrapperView都已正常释放,而MyTableViewCell没有——说明问题出在这条 cell 上(例如 cell 被某个全局缓存、block 或代理强持有)。

告警同时会输出到控制台(NSLog),便于在 Xcode 中直接查看完整信息。

六、Mute Assertion:屏蔽不应释放的类

如果某个类被设计为单例,或者出于业务原因其对象本就不应该释放,继续触发检测只会产生噪音。README 给出的标准做法是在业务类中重写willDealloc并返回NO:

- (BOOL)willDealloc { return NO; }

注意:任何子类在重写willDealloc时都应先调用[super willDealloc],当父类返回NO时直接透传NO(详见第七节示例),保证白名单、触摸对象过滤等父级判断依然生效。

除运行时重写外,框架本身还内置了一份类名白名单(见 NSObject+MemoryLeak.m):

  • UIFieldEditor(UIAlertController 的输入框)
  • UINavigationBar
  • _UIAlertControllerActionView
  • _UIVisualEffectBackdropView
  • UISwitch(仅在 iOS 10.0 及以上加入,源码注释标明这是 iOS 10 起至今未修复的系统 bug)

如果业务中有需要整类屏蔽的场景,可以使用公开 API 动态追加:

+ (void)addClassNamesToWhitelist:(NSArray *)classNames;

例如:

[NSObject addClassNamesToWhitelist:@[ @"MySingleton", @"MyCacheObject" ]];

七、扩展检测:MLCheck 自定义对象

MLeaksFinder 默认只检测 UIView / UIViewController 对象,但 README 明确指出:你可以把它扩展到以某个 UIViewController 为根的整个对象图。做法是在该控制器的willDealloc中,对需要跟踪的业务对象调用MLCheck:

- (BOOL)willDealloc { if (![super willDealloc]) { return NO; } MLCheck(self.viewModel); return YES; }

MLCheck是定义在 NSObject+MemoryLeak.h 的宏:

#define MLCheck(TARGET) [self willReleaseObject:(TARGET) relationship:@#TARGET];

其作用等价于调用willReleaseObject:relationship:,@#TARGET会把表达式原样字符串化作为关系描述(例如传self.viewModel时 relationship 为self.viewModel)。在 NSObject+MemoryLeak.m 的实现中:

  • 若 relationship 以self开头,会先去掉这个前缀(避免栈里出现无意义的self.);
  • 生成形如relationship(ClassName),的栈条目追加到子对象的viewStack;
  • 把子对象指针加入parentPtrs集合;
  • 调用子对象的willDealloc,从而复用整套"2 秒延迟二次确认"机制。

也就是说,只要你的viewModel在控制器销毁后仍存活,2 秒后就会收到带完整关系链的 "Memory Leak" 告警。检测 ViewModel、Model、网络层对象等任意 NSObject 子类都可以沿用同一模式。

八、循环引用检测(可选):FBRetainCycleDetector

MLeaksFinder 曾集成 Facebook 的 FBRetainCycleDetector,用于为泄漏对象尝试找出循环引用。但根据 README.md 中的WARNING:由于 Facebook 的 BSD-plus-Patents 许可证问题,FBRetainCycleDetector 已从 podspec 中移除(MLeaksFinder.podspec 中对应的s.dependency也处于注释状态)。

如果仍想使用循环引用检测,需要两步:

  1. 在你的项目 Podfile 中显式添加:
pod 'FBRetainCycleDetector'
  1. 在 MLeaksFinder.h 中开启宏:
#define MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 1

开启后,泄漏告警弹窗会多出一个 "Retain Cycle" 按钮(见 MLeakedObjectProxy.m);点击后,代码会在后台线程用FBRetainCycleDetector以findRetainCyclesWithMaxCycleLength:20查找循环引用,并将找到的环(从泄漏对象处 shift 对齐后)弹窗展示(见 MLeakedObjectProxy.m)。

从源码结构看,MLeaksFinder.h 中还保留了#elif COCOAPODS分支:当MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED宏未被定义、且工程经由 CocoaPods 集成(COCOAPODS宏存在)时,内部开关_INTERNAL_MLF_RC_ENABLED会跟随COCOAPODS取值。仓库自带的示例 Podfile 与 Podfile.lock 中即包含FBRetainCycleDetector(锁定版本 0.1.2),说明官方示例工程默认启用了该能力。无论走哪条分支,只要相关代码参与编译,都必须保证 FBRetainCycleDetector 头文件可找到,否则会编译失败。

需要澄清的是:"Fail to find a retain cycle" 并不代表没有泄漏。FAQ-CN.md 明确指出两种可能:其一,内存泄漏不一定是循环引用造成的(比如被全局变量强持有);其二,有些循环引用 FBRetainCycleDetector 也未必能识别。因此该功能只是辅助手段,找不到环不影响 MLeaksFinder 自身的泄漏判定。

九、常见问题排查(FAQ 精华)

综合 FAQ-CN.md 与源码,接入过程中最常见的几个问题如下:

9.1 引进 MLeaksFinder 后没生效

  • 先验证引进是否正确:在 UIViewController+MemoryLeak.m 的+ (void)load加断点,App 启动能进入则引进成功;
  • 检查 CocoaPods 安装时的 warnings,尤其是OTHER_LDFLAGS相关;有 warning 时在主工程 Build Settings → Other Linker Flags 加上-ObjC(保证 Objective-C Category 被正确链接进可执行文件)。

9.2 可以手动引进吗

可以。直接把MLeaksFinder/下的源码放入项目即可生效;若以子工程方式引入,同样需要在主工程 Other Linker Flags 加-ObjC。手动引进后泄漏检测即生效,但循环引用功能还需额外加入 FBRetainCycleDetector 代码并打开 MLeaksFinder.h 中的宏开关。

9.3 如何关闭 MLeaksFinder

默认情况下它只在 Debug 构建下生效(_INTERNAL_MLF_ENABLED取DEBUG,见 MLeaksFinder.h)。如需强制关闭,取消注释并设置为 0:

#define MEMORY_LEAKS_FINDER_ENABLED 0

Release 构建无需任何处理即不包含检测逻辑。

十、仓库结构速览

MLeaksFinder/ ├── MLeaksFinder.h # 总开关宏(ENABLED / RETAIN_CYCLE_ENABLED) ├── NSObject+MemoryLeak.{h,m} # 核心:willDealloc、MLCheck、白名单、swizzle 工具 ├── UIViewController+MemoryLeak.m # VC 生命周期钩子注入 ├── UIView+MemoryLeak.m # subviews 递归检测 ├── UINavigationController+MemoryLeak.m ├── UIPageViewController+MemoryLeak.m ├── UISplitViewController+MemoryLeak.m ├── UITabBarController+MemoryLeak.m ├── UIApplication+MemoryLeak.m # 防误报:记录最新事件 sender ├── UITouch+MemoryLeak.m # 防误报:记录最新触摸 View ├── MLeakedObjectProxy.{h,m} # 泄漏登记、去重、循环引用按钮处理 ├── MLeaksMessenger.{h,m} # UIAlertView 弹出与 NSLog 输出 ├── MLeaksFinder.xcodeproj/ # 静态库工程(产物 libMLeaksFinder.a) ├── MLeaksFinder.podspec # CocoaPods 描述(v1.0.0,iOS 6.0+) ├── Podfile / Podfile.lock # 示例工程(含 FBRetainCycleDetector 0.1.2) ├── README.md # 官方说明(本文主线) └── FAQ-CN.md # 中文 FAQ(排查指南)

整体来看,MLeaksFinder 的设计非常轻量:核心文件只有NSObject+MemoryLeak、MLeakedObjectProxy、MLeaksMessenger三份,其余 UIKit 扩展类全部是"生命周期钩子 + 子对象扩散"的薄封装。对希望深入理解 Objective-C 运行时(Method Swizzling、Associated Object)和 iOS 内存泄漏排查的开发者来说,这份源码是极佳的学习样本。

结语

MLeaksFinder 用"延迟二次确认 + 对象图递归 + 栈式告警"这套组合拳,把内存泄漏检测变成了开发期的自动化流程:接入成本接近于零,告警信息直接指向泄漏对象在视图层级中的位置,误报可通过willDealloc重写或白名单收敛,检测范围可通过MLCheck自由扩展到业务对象。无论你是想直接把它引入工程作为日常排查工具,还是希望借鉴其 Method Swizzling 与弱引用复查的思路,都可以在本文基础上继续阅读 README.md、FAQ-CN.md 与MLeaksFinder/目录下的源码逐一印证。

  • 开发工具
  • 质量保障

【免费下载链接】MLeaksFinder

Find memory leaks in your iOS app at develop time.

项目地址:https://gitcode.com/gh_mirrors/ml/MLeaksFinder
点击查看免费下载
上一篇:CampusShame服务器维护:GitHub Pages性能优化指南
下一篇:ConvNetJS 完整指南:用纯 JavaScript 在浏览器与 Node.js 中定义、训练与编译神经网络

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询