UIViewController的内存泄露检测实践

简介

内存泄露在性能优化中一直是一个老生常谈的问题,而且最常出现在UIViewController的使用过程中。一般当VC退出UI堆栈后,如果使用过的内存没有被释放,就会产生内存泄露。所以在使用UIViewController的时候需要特别注意。

内存泄露原理及解决方案

一般在VC中出现内存泄露主要原理是产生了循环引用导致内存得不到释放,而循环引用产生的主要case大概了以下三种情况:

NSTimer

在使用NSTimer的时候一般会把target设置为self,而timer本身又是self的成员变量,这样就会产生循环引用的问题。

打破循环的方式是在VC退出时停掉timer并将timer成员变量置空。

delegate

delegate产生循环引用的原理和timer类同,一般我们会将delegate修饰为weak来打破循环

block

block也是最容易产生循环引用的地方,稍不注意就会造成内存泄露。所以在使用block的时候也需要格外留神。block产生循环引用的原理主要还得从它的内存说起,这里不细说(网上讲这个case的情况不要太多)。

有2中方法可以打破block的循环引用:

  1. 使用weak修饰self
  2. 使用__block修饰self,在block内部将self置空

不过方案2有个小问题,如果该block没有被调用的话这个循环引用就没法被打破了,所以通常我们都使用第一种方案。

但是在开发业务的过程中难免会有疏漏的地方,经验丰富的老鸟还好,如果组内有小萌新的话,这种问题就很可能会出现,这里写这个工具也是做一个保护。

内存泄露检测原理

当VC被pop或者dismiss后,正常的流程是走到dealloc,然后使用的内存会被释放,self会为nil,如果有循环引用,则不会走dealloc,内存得不到释放,self也不会为nil.

这里我们就利用了释放后self会被置空,而不释放self不会为nil的原理来实现内存泄露的警告。

实现

首先我们需要hook UIViewController的下面几个方法:

  • viewWillAppear
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)swizzledViewWillAppear{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(ocn_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
  • viewDidDisappear
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)swizzledViewDidDisappear{
Class class = [self class];
SEL originalSelector = @selector(viewDidDisappear:);
SEL swizzledSelector = @selector(ocn_viewDidDisappear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
  • dismissViewControllerAnimated
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)swizzledDismissViewController{
Class class = [self class];
SEL originalSelector = @selector(dismissViewControllerAnimated:completion:);
SEL swizzledSelector = @selector(ocn_dismissViewControllerAnimated:completion:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

同时我们还需要hook 一下UINavigationController的pop方法:

  • popViewControllerAnimated
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)swizzledPopViewControllerAnimated{
Class class = [self class];
SEL originalSelector = @selector(popViewControllerAnimated:);
SEL swizzledSelector = @selector(ocn_popViewControllerAnimated:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}

这里当然选择在分类中进行hook最方便了。我们把hook的代码放在load函数中执行,具体原因参考这里.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+ (void)load{
#if defined(DATAONLINE) && DATAONLINE == 0
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzledViewWillAppear];
[self swizzledViewDidDisappear];
[self swizzledDismissViewController];
});
#endif
}
+ (void)load{
#if defined(DATAONLINE) && DATAONLINE == 0
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzledPopViewControllerAnimated];
});
#endif
}

这里的宏表示是测试环境。

在viewWillAppear中,我们记录一下当前VC的状态。

1
2
3
4
5
6
7
- (void)ocn_viewWillAppear:(BOOL)animated{
[self ocn_viewWillAppear:animated];
[self setOcn_hasBeenPopped:NO];
}

然后在viewDidDisappear时,我们判断一下当前VC的状态,如果VC被pop或者dismiss了,我们调用一下内存泄露提醒的方法

1
2
3
4
5
6
7
8
9
10
11
12
- (void)ocn_viewDidDisappear:(BOOL)animated{
[self ocn_viewDidDisappear:animated];
__weak __typeof(self)weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if ([weakSelf ocn_hasBeenPopped]) {
[weakSelf alertWithClassName:NSStringFromClass([weakSelf class])];
}
});
}

这里我们做了一个延时调用,前面已经说过,如果VC走正常流程,内存被释放,self会被置空,所以这里如果没有内存泄露的话,weakself应该是为nil,我们使用nil对象调用这两个方法不会有提醒,而如果发生了内存泄露,weakself不会为nil,这时调用者这2个方法就会弹出内存泄露的警告了。

当然,我们还需要再dismiss和pop的时候设置当前vc的状态为已pop的状态,只有是已经被pop了,才会去尝试调用提醒事件。因为当vc的调用栈被push到下一级,也就是上面有新的vc时,viewDidDisappear也会被调用,但是此时vc的内存还会保存在内存中,只有在被pop或者dismiss后,vc的内存才有可能被释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)ocn_dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
[self ocn_dismissViewControllerAnimated:flag completion:completion];
[self setOcn_hasBeenPopped:YES];
}
- (UIViewController *)ocn_popViewControllerAnimated:(BOOL)animated {
UIViewController *poppedViewController = [self ocn_popViewControllerAnimated:animated];
if (!poppedViewController) {
return nil;
}
objc_setAssociatedObject(poppedViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN);
return poppedViewController;
}

后续

这个工具目前只能检测UIViewController的内存泄露问题,后续还可以再加上其他类型的检测方法。写这个工具也算是对runtime学习的一个实践。

这里是demo