官方文档介绍 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
会记录从上回内存快照到这次内存快照这个时间段内,新分配的内存信息.
Xcode 运行项目时,切换到 Debug navigator
点击 memory
就可以查看 Memory Report
,显示 内存使用 的整体情况:
用于定位内存泄露的话用处不大,只能看到内存的概况。
分析案例:
缺陷:只能检查编译时的内存泄漏,并不能检测到所有的内存泄漏,如:发生在运行时,或需要用户操作时产生的泄露。
首先,修改编译设置生成符号信息,以便 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
用 Xcode 把 app 跑起来。打开Leaks:
入口在菜单栏:Xcode -> Open Developer Tool -> Instruments -> 然后选择 Leaks -> Choose
(打开操作面板)
步骤1:选好设备和需要测试的 app
步骤2:点击同行最左边的红色按钮,开始录制(点击开始录制会重启 app)
录制过程中:
默认选择的是 Lesks 页面,下半部分显示的是泄露的详情,左边是目前为止检测到的所有泄露;选中其中一个,右侧显示的是泄露点的调用堆栈,可据此找到泄露点进行修改。
底部栏:
也可选择时间段过滤:在起始时间点
按下鼠标左键,拖动到截止时间点
松开:
点击中间栏的左侧切换到Cycles & Roots
页面,可查看泄露图:
看图分析应该是因为block
导致的循环引用,按调用堆栈找到对应的代码:
点击中间栏的左侧切换到Call Tree
统计模式,也可通过底部栏的工具进行过滤
Separate By Thread
:线程分离,在调用路径中能够清晰看到占用内存最大的线程
Invert Call Tree
:反转调用堆栈顺序
Hide System Libraries
:隐藏系统库的调用堆栈信息
Flatten Recursion
:会将调用栈里递归函数作为一个入口(很少使用)
底部栏可设置各种约束进行过滤(用的比较少):
按符号过滤 or 按库过滤
可显示当前所有 已使用内存 的详情
Malloc Scribble
:开启将使用预定义的值填充释放的内存,从而在内存泄漏时更加明显。这提高了Xcode识别泄漏的准确性。
Malloc Stack Logging
:启用此选项将允许Xcode构建分配回溯,以帮助了解对象从何处引用。
Xcode 运行项目时可点击中部栏的Debug Memory Graph
按钮,查看内存图:
点击左侧 导航栏 - 底部栏 的 Show only leaked allocations
按钮,可过滤出泄露的对象:
退出页面后点击 Debug Memory Graph
,在底部Filter
栏输入 关键字 过滤出当前还存活的对象,进行分析:
以上介绍的都是 Xcode 自带的可视化工具,下面介绍的是其他代码检测工具。
Facebook 开源的 循环引用检测 工具 FBRetainCycleDetector
当确认或怀疑一个对象是否泄露时,都可以使用该工具查找循环引用链。
1). main 里添加对 objc_setAssociatedObject
的查找:
#import <FBRetainCycleDetector/FBAssociationManager.h>
int main(int argc, char * argv[]) {
@autoreleasepool {
[FBAssociationManager hook];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
2). 泄露后查找引用环:
#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
腾讯视频已集成的 RaftKit (未开源)里的有 内存泄露监控 工具(底层用的是Bugly):
打开 RaftKit 在内存泄露工具里,查看内存泄露记录文件:
内部也是使用FBRetainCycleDetector
进行引用循环链的查找:
也可将文件导出:FloatingWebVC.txt
分析详情中的循环引用链
:左边是实例名,右边实例的类型;从第一个到最后一个形成了一个引用环。
找到对应的类进行分析:
QNBUALiveShowLayoutBridgeBase
是持有 jsBridge
的,且 jsBridge
又间接持有该 block,所以在 block 里直接使用 self 就形成了引用环了。
(26个Handler,95% block 的写法都导致了循环引用)
没有引用环的,可以打开 Memory graph 分析被谁持有的。
Tencent 的开源检测内存泄露库:MLeaksFinder
可在日常开放中默认打开,以便及时获得泄露警告,而不用特意打开以上工具去排查。
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
)
如下所示,在第一次 pop 时报了 Memory Leak
,在之后重复 push 并 pop 同一个 ViewController 过程中,即不报 Object Deallocted
,也不报 Memory Leak
。这种情况可以确定该对象是被设计成单例 or 被 cache 起来了。
pop push pop push pop
----------> Leak ----------> | ----------> | ----------> | ---------->
如下所示,在第一次 pop 时报 Memory Leak
,在之后的重复 push 和 pop 同一个 ViewController 过程中,对于同一个类不断地报 Object Deallocated
和 Memory Leak
。这种情况属于释放不及时。
pop push pop push pop
----------> Leak ----------> Dealloc ----------> Leak ----------> Dealloc ----------> Leak
如下所示,在第一次 pop 时报 Memory Leak
,在之后的重复 push 和 pop 同一个 ViewController 过程中,不报 Object Deallocated
,但每次 pop 之后又报 Memory Leak
。这种每次进入并退出一个页面后都报内存泄露,且被报泄露对象又从来没有释放过,可以确定是真正的内存泄露。
pop push pop push pop
----------> Leak ----------> | ----------> Leak ----------> | ----------> Leak
MLeaksFinder
里也用了FBRetainCycleDetector
来找找循环引用链:
MEMORY_LEAKS_FINDER_ENABLED
控制是否启用FBRetainCycleDetector
查找循环引用链;
_INTERNAL_MLF_RC_ENABLED
设置alert弹框
是否显示Retain Cycle
按钮;
也可以打开 Memory graph 分析被谁持有的。
为NSObject
新增一个-willDealloc
方法:在 2s 后给弱引用的self
发送assertNotDealloc
消息:
若self
被释放则不会执行;
若self
未被释放则会执行assertNotDeall
。
然后在UIViewController
的dismiss
方法里调用willDealloc
:遍历 childVCs
、presentVCs
和subViews
触发他们的willDealloc
方法检测是否有泄露:
MLeaksFinder
目前只检测 ViewController 跟 View 对象。为此,MLeaksFinder
提供了一个手动扩展的机制,开发者可以从 UIViewController 跟 UIView 出发,去检测其它类型的对象的内存泄露。如下所示,可以检测 UIViewController 持有的 View Model:
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
MLCheck(self.viewModel);
return YES;
}
通过排查腾讯视频直播间的整体泄露后,发现泄露类型基本都是以下5类:
Block 会强引用捕获到的对象,如果该对象 直接 或 间接 强引用该 Block,则会导致循环引用:
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 (最佳实践推荐 6.1)
malloc
申请的内存没有使用 free
释放,用 Leaks
检测比较方便:
调用了 CFBridgingRetain
进行 +1
持有后,没有调用 CFBridgingRelease
进行 -1
的:
例如:用了一个static静态变量记录了上一次滑动的 scrollView,导致退出页面后改 scrollView 没有被释放
/// 记录用户最后滑动的 scrollView (case: 刚拖拽完tab1,立马切换到tab2)
static UIScrollView *gCurrentScrollView = nil;
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
...
gCurrentScrollView = scrollView;
...
}
- (BOOL)enableHandleScrollView:(UIScrollView *)scrollView {
...
if (![gCurrentScrollView isEqual:scrollView]) {
return NO; /// 已经切换tab了,还收到其他tab的回调,不处理
}
...
}
修复方案:可以使用代理类若引用该 scrollView:
/// 记录用户最后滑动的 scrollView (case: 刚拖拽完tab1,立马切换到tab2)
static QLWeakProxy *gCurrentScrollViewProxy = nil;
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
...
gCurrentScrollViewProxy = [[QLWeakProxy alloc] initWithTarget:scrollView];
...
}
- (BOOL)enableHandleScrollView:(UIScrollView *)scrollView {
...
if (![gCurrentScrollViewProxy.target isEqual:scrollView]) {
return NO; /// 已经切换tab了,还收到其他tab的回调,不处理
}
...
}
Memory Report
:只能看到内存使用的整体情况,用处不大
Analyze
:只能检查编译时期的内存泄漏,不能检测运行时产生的泄露
Leaks
:适合发现持续的泄露
Memory Graph
:适合发现退出后没有释放的内存泄露
FBRetainCycleDetector
:用于查找循环引用链,搭配其他查找泄露对象工具使用
MLeaksFinder
:可查找VC和View的泄露,代码开源也可进行DIY拓展
iOS内存泄漏检查&原理
iOS内存分析原理
检测和诊断 App 内存问题
MLeaksFinder
MLeaksFinder 新特性
MLeaksFinder:精准 iOS 内存泄露检测工具
MLeaksFinder 原理