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 原理