1、内存分类
官方文档介绍 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:app 没有引用的内存,无法再次使用或释放(可以使用 Leaks 工具检测)Abandoned memory:app 仍有引用,但没有任何用途的内存Cached memory:app 仍有引用,可能会再次使用以获得更好的性能
Leaked memory 和 Abandoned memory 都是应该释放而没释放的内存,属于内存泄露。
Leaked memory 可以用 Instrument 的 Leaks 检测出来。Leaks的实现思路是搜索所有可能包含指向 malloc 内存块指针的内存区域,比如全局数据内存块,寄存器和所有的栈。如果 malloc 内存块的地址被直接或者间接引用,则是 reachable 的,反之则是 leaks。
Abandoned memory可以用 Instrument 的 Allocations 检测出来。检测方法是用 Mark Generation 的方式,当每次点击 Mark Generation 时,Allocations 会生成当前 App 的内存快照,而且 Allocations 会记录从上回内存快照到这次内存快照这个时间段内,新分配的内存信息.
2、Memory Report
Xcode 运行项目时,切换到 Debug navigator 点击 memory 就可以查看 Memory Report,显示内存使用的整体情况:

用于定位内存泄露的话用处不大,只能看到内存的概况。
3、Analyze
静态分析入口:

分析案例:
缺陷:只能检查编译时的内存泄漏,并不能检测到所有的内存泄漏,主要是因为有些泄漏是发生在运行时,或需要用户操作才会产生。
4、Leaks
4.1、前置设置
首先,修改编译设置生成符号信息,以便 Leaks 分析出调用堆栈函数符号:
Target -> Build Settings -> Build Options -> Debug Information Format -> Debug -> DWAPR with dSYM File
否则 Leaks 无法解析调用堆栈函数名:
no stack trace is available for this leak; it may have been allocated before the Allocation instrument was attached
将 app 通过 Xcode 装到手机上后,入口在菜单栏:Xcode -> Open Developer Tool -> Instruments -> 然后选择 Leaks -> Choose (打开操作面板)


4.2、页面介绍
步骤1:选好设备和需要测试的 app
步骤2:点击同行最左边的红色按钮,开始录制(点击开始录制会重启 app)

录制过程中,左边按钮是停止,右边按钮是暂停:

Leaks 录制过程中会出现3种标志:
- 绿色:没有发现泄露
- 红色:发现新的泄露
- 灰色:没有发现新的泄露

4.3、使用
4.3.1、Leaks
下半部分显示的是泄露的详情,左边是目前为止检测到的所有泄露;选中其中一个,右侧显示的是泄露点的调用堆栈,可据此找到泄露点进行修改。

底部栏:
-
snapshots,可以设置检测泄露的时间间隔,也有立即检测按钮:

-
Input Filter可通过线程过滤 -
Detail Filter可通过关键字过滤

也可选择时间段过滤:在起始时间点按下鼠标左键,拖动到截止时间点松开:

4.3.2、Cycles & Roots:
点击中间栏的左侧切换到Cycles & Roots模式,可查看泄露图:

看图分析应该是因为block导致的循环引用,按调用堆栈找到对应的代码:

4.3.3、Call Tree:
点击中间栏的左侧切换到Call Tree统计模式,也可通过底部栏的工具进行过滤
Separate By Thread:线程分离,在调用路径中能够清晰看到占用内存最大的线程
Invert Call Tree:反转调用堆栈顺序
Hide System Libraries:隐藏系统库的调用堆栈信息
Flatten Recursion:会将调用栈里递归函数作为一个入口(很少使用)

底部栏可设置各种约束进行过滤(用的比较少):
按符号过滤 or 按库过滤

设置最大最小值进行过滤:

设置 符号/库 变化时/删减掉 进行过滤:

5、Memory Graph
5.1、介绍
1)修改配置:

Malloc Scribble:开启将使用预定义的值填充释放的内存,从而在内存泄漏时更加明显。这提高了Xcode识别泄漏的准确性。
Malloc Stack Logging:启用此选项将允许Xcode构建分配回溯,以帮助了解对象从何处引用。
Xcode 运行项目时可点击中部栏的Debug Memory Graph按钮,查看内存图:

2)入口:
再点击左侧 导航栏 - 底部栏 的 Show only leaked allocations 按钮,可过滤出泄露的对象:

或者在底部Filter栏输入前缀过滤出当前还存在的对象进行分析:

3)使用graph分析:
5.2、使用方式1:
直接通过 Show only leaked allocations 过滤出明显的泄露进行修复
例如:动画用到的 CGPath 没有释放:

5.3、使用方式2:
退出页面后点击 Debug Memory Graph,分析没有释放的对象,是否为内存泄露
例如:退出直播间应该释放的插件没有释放:

以上介绍的都是 Xcode 自带的可视化工具,下面介绍的是其他代码检测工具。
8、FBRetainCycleDetector
Facebook 的开源循环引用检测工具 FBRetainCycleDetector
当确认或怀疑一个对象是否泄露时,都可以使用该工具查找循环引用链,在合适的位置加入查找逻辑: (如:VC的didAppear 或 其他对象确认已经投入使用时)
#import <FBRetainCycleDetector/FBRetainCycleDetector.h>
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:self];
NSSet *retainCycles = [detector findRetainCycles];
NSLog(@"retain cycle: %@ %@", [self class], retainCycles);
输出,例如:
(
"-> MyTableViewCell ",
"-> _callback -> __NSMallocBlock__ "
)
表示:cell 持有 block,block 持有 cell
8、RaftKit
腾讯视频已集成的 RaftKit (未开源)里的有 内存泄露监控 工具(底层用的是Bugly):

打开开关和提示弹框:

打开后,当发现泄露会弹出alert:

打开 RaftKit 在内存泄露工具里,查看内存泄露记录文件:

点击需要分析的泄露对象,查看详情:

内部也是使用FBRetainCycleDetector进行引用循环链的查找:

也可将文件导出:FloatingWebVC.txt
分析详情中的循环引用链:左边是实例名,右边实例的类型;从第一个到最后一个形成了一个引用环。
找到对应的类进行分析:

QNBUALiveShowLayoutBridgeBase 是持有 jsBridge 的,且 jsBridge 又间接持有该 block,所以在 block 里直接使用 self 就形成了引用环了。
(26个Handler,95% block 的写法都导致了循环引用)
9、MLeaksFinder
Tencent 的开源检测内存泄露库:MLeaksFinder
可在日常开放中默认打开,以便及时获得泄露警告,而不用特意打开以上工具去排查。
9.1、使用:
podfile里添加导入,然后执行 pod install:
pod 'MLeaksFinder'
pod 'FBRetainCycleDetector'
使用 MLeaksFinder.h 的宏 MEMORY_LEAKS_FINDER_ENABLED 控制该工具是否可用.
当 MLeaksFinder 发现内存泄露时会弹出 Memory Leak 的 alert :
Memory Leak
(
MyTableViewController,
UITableView,
UITableViewWrapperView,
MyTableViewCell
)
表示:MyTableViewController,UITableView,UITableViewWrapperView 都已成功释放,但其 subView MyTableViewCell 没有释放。
并会持续追踪该对象的生命周期,并在该对象释放时给出 Object Deallocated 的 alert :
Object Deallocated
(
MyTableViewController,
UITableView,
UITableViewWrapperView,
MyTableViewCell
)
9.2、分析 alert:
情形1、单例 or 被 cache 起来的对象
如下所示,在第一次 pop 时报了 Memory Leak,在之后重复 push 并 pop 同一个 ViewController 过程中,即不报 Object Deallocted,也不报 Memory Leak。这种情况可以确定该对象是被设计成单例 or 被 cache 起来了。
pop push pop push pop
----------> Leak ----------> | ----------> | ----------> | ---------->
情形2、释放不及时
如下所示,在第一次 pop 时报 Memory Leak,在之后的重复 push 和 pop 同一个 ViewController 过程中,对于同一个类不断地报 Object Deallocated 和 Memory Leak。这种情况属于释放不及时。
pop push pop push pop
----------> Leak ----------> Dealloc ----------> Leak ----------> Dealloc ----------> Leak
情形3、真正的泄露
如下所示,在第一次 pop 时报 Memory Leak,在之后的重复 push 和 pop 同一个 ViewController 过程中,不报 Object Deallocated,但每次 pop 之后又报 Memory Leak。这种每次进入并退出一个页面后都报内存泄露,且被报泄露对象又从来没有释放过,可以确定是真正的内存泄露。
pop push pop push pop
----------> Leak ----------> | ----------> Leak ----------> | ----------> Leak
9.3、查找循环引用链:
MLeaksFinder里也用了FBRetainCycleDetector来找找循环引用链:
MEMORY_LEAKS_FINDER_ENABLED控制是否启用FBRetainCycleDetector查找循环引用链;
_INTERNAL_MLF_RC_ENABLED设置alert弹框是否显示Retain Cycle按钮;
9.4、扩展:
MLeaksFinder 目前只检测 ViewController 跟 View 对象。为此,MLeaksFinder 提供了一个手动扩展的机制,开发者可以从 UIViewController 跟 UIView 出发,去检测其它类型的对象的内存泄露。如下所示,可以检测 UIViewController 持有的 View Model:
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
MLCheck(self.viewModel);
return YES;
}
9.5、原理
为NSObject新增一个-willDealloc方法:在 2s 后给弱引用的self发送assertNotDealloc消息:
若self被释放则不会执行;
若self未被释放则会执行assertNotDeall。

然后在UIViewController的dismiss方法里调用willDealloc:遍历 childVCs、presentVCs和subViews触发他们的willDealloc方法检测是否有泄露:

10、泄露总结:
通过排查腾讯视频直播间的整体泄露后,发现泄露类型基本都是以下5类:
10.1、Block
10.2、NSTimer
NSTimer 为什么这么容易导致内存泄露:
很重要的一点是因为 RunLoop 会强引用 NSTimer(系统实现的无法做修改)。
所以开发者必须在恰当的时机将NSTimer释放掉。
而一般最佳释放时机为持有 NSTimer 的 self 的 dealloc 方法里:
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
iOS10之前的方法,需要传入target(一般我们用self)作为代理,执行需要定时触发的方法。
因为NSTimer会强引用传入的target(这也是系统实现的无法修改)。
当开发者直接传入 self 时,就导致了 self 无法被释放,进而在 dealloc 里释放 NSTimer 的代码也不会执行,从而导致了内存泄露:RunLoop -> NSTimer -> self (不是引用环,但是无法释放)
iOS10苹果新出了3个方法,采用block的形式实现代理方法,不需要传入self(block中还是需要用weakSelf),从而保证了self的dealloc的执行。
更多计时器介绍可见:iOS_定时器:NSTimer、GCDTimer、DisplayLink
10.3、malloc -> free
malloc 申请的内存没有使用 free 释放,用 Leaks 检测比较方便:

10.4、CFBridgingRetain - CFBridgingRelease
调用了 CFBridgingRetain 进行 +1 持有后,没有调用 CFBridgingRelease 进行 -1 的:

10.5、单例滥用
一个点赞动效使用了单例,退出直播间没有释放:

11、工具总结:
Memory Report:只能看到内存使用的整体情况,用处不大
Analyze:只能检查编译时期的内存泄漏,不能检测运行时产生的泄露
Leaks:适合发现持续的泄露
Memory Graph:适合发现退出后没有释放的内存泄露
FBRetainCycleDetector:用于查找循环引用链,搭配其他查找泄露对象工具使用
MLeaksFinder:可查找VC和View的泄露,代码开源也可进行DIY拓展
参考:
iOS内存泄漏检查&原理
iOS内存分析原理
检测和诊断 App 内存问题
MLeaksFinder
MLeaksFinder 新特性
MLeaksFinder:精准 iOS 内存泄露检测工具
MLeaksFinder 原理


















