iOS内存管理:析构检测原理
一、前言
在iOS日常开发中,很多不经意的小错误都会导致内存泄漏,从而影响项目的稳定性。
除了最常见的循环引用外,还有下面几种常见错误会造成内存泄漏。
1 使用c语言方法,需要手动释放
2 对文件进行操作,要手动关闭
3 调用block时,给block传指nil
4 数组中插入了nil元素
二、检测
日常项目中常用的内存泄漏检测:
1 静态检测,使用Xcode,Product->Analyze静态分析
2 动态检测,使用Intruments->Leaks动态分析
3 析构检测,通过Runtime(AOP)监听系统dealloc方法
当然,还有像MLeaksFinder这类封装好的内存泄漏检测工具,是基于析构检测的一款非常好用且全面的工具。
本文介绍在视图控制器Push/Pop操作中如何监听dealloc方法达到内存泄漏检测的目的,也是MLeaksFinder实现的基础。
三、原理
我们都知道在控制器正常的生命周期中会在viewDidDisappear:
之后执行dealloc
方法,从而进行内存的回收和释放。但是如果控制器中存在循环引用之类的问题,会导致控制器的dealloc
方法无法执行,造成内存泄漏。
析构检测的原理就是监听控制器的生命周期,在viewDidDisappear:
时手动模拟dealloc方法,延迟检测控制器有没有被回收。
四、关键代码
使用Runtime Method-Swizzling避免对项目代码的侵入:
+ (void)swizzleSelector:(SEL)orignSelector with:(SEL)swizzleSelector {
Class class = [self class];
Method orignalMethod = class_getInstanceMethod(class, orignSelector);
Method swizzleMethod = class_getInstanceMethod(class, swizzleSelector);
BOOL didAddMethod = class_addMethod(class, orignSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
class_replaceMethod(class, swizzleSelector, method_getImplementation(orignalMethod), method_getTypeEncoding(orignalMethod));
}else {
method_exchangeImplementations(orignalMethod, swizzleMethod);
}
}
下面是模拟的dealloc方法,通过一个弱引用持有self,然后进行一个系统延迟执行的操作,在这个延迟的过程中,如果没有发生内存泄漏,系统应该会执行本身的dealloc方法将self回收,此后的打印应该为nil,如果控制器发生内存泄漏,导致dealloc方法没有执行,此时就会打印出相应发生内存泄漏的控制器。
在MLeaksFinder中,延迟后执行的是断言操作。
- (void)willDealloc {
__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;
NSLog(@"leak : %@", NSStringFromClass([strongSelf class]));
});
}
五、监控生命周期
在UIViewController
的分类中:
const void *const kHasBeenPoppedKey = &kHasBeenPoppedKey;
@implementation UIViewController (Leaks)
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSelector:@selector(viewWillAppear:) with:@selector(yy_viewWillAppear:)];
[self swizzleSelector:@selector(viewDidDisappear:) with:@selector(yy_viewDidDisappear:)];
});
}
// 标记入栈
- (void)yy_viewWillAppear:(BOOL)animated {
[self yy_viewWillAppear:animated];
objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}
// 查看标记情况,模拟dealloc方法
- (void)yy_viewDidDisappear:(BOOL)animated {
[self yy_viewDidDisappear:animated];
if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
[self willDealloc];
}
}
@end
在UINavigationController
的分类中:
@implementation UINavigationController (Leaks)
+(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSelector:@selector(popViewControllerAnimated:) with:@selector(yy_popViewControllerAnimated:)];
});
}
// 标记出栈
- (UIViewController *)yy_popViewControllerAnimated:(BOOL)animated {
UIViewController *targetViewController = [self yy_popViewControllerAnimated:animated];
extern const void *const kHasBeenPoppedKey;
objc_setAssociatedObject(targetViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN);
return targetViewController;
}
@end
至此,一个简单的dealloc检测就基本实现了,你可以在控制器中模拟一个循环引用测试内存泄漏。
六、测试
我们都知道在使用NSTimer
时,因为self.timer被控制器强引用,与此同时self.timer又持有self。这就是一个简单的循环引用。导致控制器退出时系统并不会调用dealloc方法释放self.timer。(解决方法这里不展开,有很多处理方式,比如:proxy)
// TargetViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(runloop) userInfo:nil repeats:YES];
}
- (void)runloop{
NSLog(@"run");
}
最终,我们在退出TargetViewController
时,会打印:
2018-11-03 15:32:47.164322+0800 HookDealloc[66621:3093836] leak : TargetViewController
我们就可以去相应的控制器去排查问题了,假如实际开发中,控制器的逻辑比较复杂,这时就可以配合静态分析或者动态分析一起排查。