内存泄露
iOS 内存分区
高地址
栈区
临时变量,参数等
系统管理,可读可写
FILO 先进后出
连续的内存地址
地址 0x7开头
堆区
alloc,malloc 申请分配地址
手动管理内存,可读可写
不连续的内存地址
链表结构
先进先出 FIFO
内存空间分配灵活,不过也会导致内存碎片
用户管理内存,可能导致内存泄露
内存分配:动态, 速度比栈等都慢
地址 常以0x6 开头
全局/静态区
未分配全局变量区 .bss
已分配全局变量区 .data
以 0x1 开头
退出app时销毁,可读可写
运行中对象内存一直在
常量区
编译时加载,退出app时销毁
代码中的常量申请内存
只读
代码区
二进制代码数据
只读
编译时加载,退出app时销毁
保留区
低地址
内存泄露检测工具:MLeaksFinder +
FBRetainCycleDetector
MLeaksFinder
腾讯开源,把库引入项目就可以了。无需设置
存在泄露时会弹出提示框
主要检测 UIViewController 和 UIView 对象的内存泄露
其它对象类型检测需要自己扩展
默认在debug模式下,不影响线上环境
MLeaksFinder 依赖 FBRetainCycleDetector(脸书 开源循环检测)
FBRetainCycleDetector 依赖 fishhook
原理
步骤1:
UINavigationController+MemoryLeak
Navi
通过hook 导航的pop 和 push 方法:
pushViewController,
popToViewController
再通过 对象绑定方法 记录控制器 入栈,出栈状态:
extern const void *const kHasBeenPoppedKey;
objc_setAssociatedObject(poppedViewController,
kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN);
通过标记控制器是否被pop,根据这个判断是否需要检测控制器里对象, 否则不
检查。
步骤2:
UIViewController+MemoryLeak
VC
hook UIViewController 的 显示和消失方法:
viewDidDisappear:
viewWillAppear:
dismissViewControllerAnimated:completion:
对子控制器和父控制器 都进行释放状态判断:
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
结合 导航文件中对控制器状态的标记,获取:
objc_getAssociatedObject([NSObject class], kDelegateKey);
判断是否是 出栈状态,如果是,继续往下走
控制器 被 pop了 && (dismiss 或 disappear) 表示需要检测,默认3秒后判断
获取VC 的实例变量:
unsigned int count;
Ivar *ivars = class_copyIvarList([self class], &count);
同时还要获取实例变量的标记状态:
id<MLeaksFinderDelegate> delegate =
objc_getAssociatedObject([NSObject class], kDelegateKey);
判断标记状态是需要检测循环引用的话,再走下一步:
调用 [self willReleaseChild:ivarObject]; 方法,
其内部调用:
willDealloc 方法
willDealloc里使用 MLeakedObjectProxy 管理类的一个方法(判断释放泄
露):
isAnyObjectLeakedAtPtrs:
这个判断方法使用的就是FBRetainCycleDetector 框架
FBRetainCycleDetector 是Facebook 开源的检测对象内存泄露的
步骤3
最终把泄露对象及响应链向底层对象 通过弹框形式弹出
流程
文件
项目主要是一些分类文件
NSObject+MemoryLeak
UIViewController+MemoryLeak
UIView+MemoryLeak
UINavigationController+MemoryLeak
UITouch+MemoryLeak
UITabBarController+MemoryLeak
UIApplication+MemoryLeak
等等
为基类 NSObject 添加一个方法 -willDealloc 方法
在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -
assertNotDealloc 主要作用是直接中断言。
- (BOOL)willDealloc {
__weak id weakSelf = self; // 若引用
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 *
NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 延迟3秒后,如果这个对象还没有释放,这个方法就会被调用
[weakSelf assertNotDealloc];
});
return YES;
}
- (void)assertNotDealloc {
NSAssert(NO, @“”); // 处理异常等
}
遍历相关对象,获取泄露对象
当控制器走到 -willDealloc 方法后,对VC,UIView,及其subViews等等进行遍
历,并调用 -willDealloc 方法
例如 UINavigationController willDealloc 时就会调用 [self
willReleaseChildren:self.viewControllers];
UIView (MemoryLeak)
@implementation UIView (MemoryLeak)
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.subviews];
return YES;
}
@end
构建堆栈信息
对应泄露信息弹框,尽量带上 对象对父节点信息等,方便定位问题
特殊情况
例外机制
对于有些 ViewController,在被 pop 或 dismiss 后,不会被释放(比如单
例),因此需要提供机制让开发者指定哪个对象不会被释放,这里可以通过重载
上面的 -willDealloc 方法,直接 return NO 即可
特殊情况
对于某些特殊情况,释放的时机不大一样(比如系统手势返回时,在划到一半时
hold 住,虽然已被 pop,但这时还不会被释放,ViewController 要等到完全
disappear 后才释放),需要做特殊处理,具体的特殊处理视具体情况而定。
白名单
某些系统的私有 View,不会被释放(可能是系统 bug 或者是系统出于某些原因
故意这样做的,这里就不去深究了),因此需要建立白名单
扩展对象支持
对一些model等想要支持的类,可以通过支持下面方法:
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
MLCheck(self.viewModel);
return YES;
}
如何检测循环引用的,看下面的FBRetainCycleDetector 实现
FBRetainCycleDetector
是如何实现内存泄露检测的呢?
分三种处理逻辑: NSTimer ,block,其它object
简单理解就是,将对象的强引用关系,形成树,被检测obj封装成根结点,对树进
行深度遍历(默认深度10):
根结点入栈,开始深度遍历,
只要有子结点,就判断栈里这条路径是否已经存在这个子节点,没有就入栈,如
果有就说明存在循环。
当一个结点没有子结点时,出栈,继续栈顶结点相同操作,直到有环,或者结点
全部出栈。
所以,关键就是:
1.获取对象强引用列表,
2.深度递归,默认最大深度10,
使用栈记录,判断遍历链中是否存在相同对象地址,存在就形成环
其中,深度遍历 比较好理解,主要就是获取强引用对象列表(当然还有很多复杂
逻辑,这里不讨论)
疑问?
如何获取对象对强引用对象列表
这个比较复杂了,先看返回强引用列表片段:
NSMutableArray *array = [NSMutableArray array];
for (auto &key: *refs) {
id value = objc_getAssociatedObject(object, key);
if (value) {
[array addObject:value];
}
}
return array;
强引用
只有三种编码字符串存在强引用关系:
【@ 】表示 obj
【@?】block
【 { 】Structrue
- (FBType)_convertEncodingToType:(const char *)typeEncoding {
if (typeEncoding[0] == '{') return FBStructType;
if (typeEncoding[0] == '@') {
if (strncmp(typeEncoding, "@?", 2) == 0) return FBBlockType;
return FBObjectType;
}
return FBUnknownType;
}
1. 常规对象 获取强引用成员变量
class_getIvarLayout
@interface XXObject: NSObject
@property(nonatomic, strong) id first;
@property(nonatomic, weak) id second;
@property(nonatomic, strong) id third;
@property(nonatomic, strong) id forth;
@property(nonatomic, weak) id fifth;
@property(nonatomic, strong) id sixth;
@end
class_getIvarLayout 返回的布局字符串为\x01\x12\x11,
如何解析这个字符串?
\x01\x12\x11 =
\x0(no)1(yes) \x1(no)2(yes) \x1(no)1(yes)
也就是左边数字表示非强引用,右边表示强引用
一共 1+2+1 = 4个强引用成员变量
ivar_getOffset
ivar_getOffset可以获取成员变量在类结构中的偏移地址,通过偏移地址找到成
员变量
将对应顺序对成员变量记录到列表,最后返回
2. 关联属性 获取强引用成员变量
FBRetainCycleDetector在对关联对象进行追踪时,通过fishhook第三方库hook
了关联对象的两个C函数,objc_setAssociatedObject 和
objc_removeAssociatedObjects,然后重新实现了关联对象模块,将通过
OBJC_ASSOCIATION_RETAIN和OBJC_ASSOCIATION_RETAIN_NONATOMIC
策略,保存起来,只追踪强引用的属性。
3. block 获取强引用成员变量
对于block持有的强引用变量的获取,依据block引用的对象总是基于block地址
偏移整个结构体的size,并且被持有的对象按照强引用在前,弱引用在后的顺序排
列,因为block强引用的对象都会进行copy到堆上和release对象引用的操作,因
此可以通过接收类FBBlockStrongRelationDetector构造detector对象,然后用
block的dispose_helper方法调用,判断如果detector对象调用release方法,就
说明当前对象是强引用对象,然后获取block持有的所有强引用变量的集合
4. NSTimer 强引用
timer比较特殊,还需要获取 target 关系:
_timer 会持有target,如果target也持有_timer,会造成引用循环