内存泄漏检测好手MLeaksFinder
原理
一般而言, 当一个 UIViewController
被完全 pop
或者 dismiss
掉后, UIViewController
本身、 UIViewController
的 view
、UIViewController
的 viewControllers
、view
的 subViews
以及 subViews
的 subViews
等等都应该在短时间内被释放掉. 基于此我们构思到:
- 找到一个时机去检测这些对象是否还存在
- 如何检测这些对象是否还存在
- 发现该被释放的对象还存在时如何提示开发者
- 规避那些被设计为单例的全局对象
- 检测完当前对象后, 继续检测它的子view或者子控制器甚至其它属性
1. 找到一个时机去检测这些对象是否还存在
这个时机其实也就是一个 UIViewController
被完全 pop
或者 dismiss
时, 于是我们 hook
掉 UINavigationController
所有的 pop
系列方法和 UINavigationController
的一个 push
方法 (Detail VC in UISplitViewController is not dealloced until another detail VC is shown) 还有 UIViewController
的 dimiss
方法, 且由于系统右滑手势返回时,可能在划到一半时hold住,虽然已被 pop
,但这时还不会被释放,ViewController
要等到完全 disappear
后才释放,所以再 hook
掉 ViewController
的 didDisappear
和 viewWillAppear
这两个方法. 这时, 已经找到了检测时机.
@implementation UINavigationController (MemoryLeak)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSEL:@selector(pushViewController:animated:) withSEL:@selector(swizzled_pushViewController:animated:)];
[self swizzleSEL:@selector(popViewControllerAnimated:) withSEL:@selector(swizzled_popViewControllerAnimated:)];
[self swizzleSEL:@selector(popToViewController:animated:) withSEL:@selector(swizzled_popToViewController:animated:)];
[self swizzleSEL:@selector(popToRootViewControllerAnimated:) withSEL:@selector(swizzled_popToRootViewControllerAnimated:)];
});
}
@implementation UIViewController (MemoryLeak)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(swizzled_viewDidDisappear:)];
[self swizzleSEL:@selector(viewWillAppear:) withSEL:@selector(swizzled_viewWillAppear:)];
[self swizzleSEL:@selector(dismissViewControllerAnimated:completion:) withSEL:@selector(swizzled_dismissViewControllerAnimated:completion:)];
});
}
2. 如何检测这些对象是否还存在
那么如何在这些 hook
掉的方法中, 检测对象是否还存在呢? 具体的方法是,为基类 NSObject
添加一个 -willDealloc
方法,并在 上述 hook
掉的方法中调用 -willDealloc
这个方法. 该方法的作用是,先用一个弱指针指向 self
,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc
,那么如果3秒后对象成功释放,weakSelf
就会指向 nil
,也就不会调用到 -assertNotDealloc
方法, 就不会有任何影响, 但如果它没被释放,-assertNotDealloc
就会被调用, 那么我们就成功将内存泄漏给找到了.
- (BOOL)willDealloc {
NSString *className = NSStringFromClass([self class]);
if ([[NSObject classNamesWhitelist] containsObject:className])
return NO;
NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;
__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;
[strongSelf assertNotDealloc];
});
return YES;
}
- (void)assertNotDealloc {
if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
return;
}
[MLeakedObjectProxy addLeakedObject:self];
NSString *className = NSStringFromClass([self class]);
NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@", className, className, [self viewStack]);
}
3. 发现该被释放的对象还存在时如何提示开发者
在 MLeaksFiner
0.1 版本中, 当 MLeaksFinder
发现内存泄漏时, 会直接中 assert
并打出内存泄漏的信息. assert
能迫使开发者及时地去修复内存泄漏, 并且, 如果只是打日志, 内存泄漏的日志很可能会被淹没在众多的日志中.
然而, assert
也有不好的一面. 当开发者在调试业务逻辑的过程中, 如果由于内存泄漏中 assert
而使得整个程序挂掉了, 那么开发者的思维会因此被打断, 并不得不在修复完内存泄漏之后, 从头开始调试业务逻辑. 有时候开发者更希望的是连贯地调完整个业务逻辑之后, 再回过头来修复内存泄漏.
因此, MLeaksFinder
0.2 版本中 把 assert
改成了 alert
. 当发现内存泄漏之后, 开发者可以把 alert
框关掉, 并继续调试业务逻辑.
4. 规避那些被设计为单例的全局对象
可以看到在 -willDealloc
中如果当前类被添加到白名单中, 则不会去检测内存泄露. MLeaksFiner
默认添加了一些白名单, 且提供了接口给开发者添加, 在 NSObject+MemoryLeak
中有 + (void)addClassNamesToWhitelist:(NSArray *)classNames
用于添加白名单
- (BOOL)willDealloc {
// 这里有白名单判断
NSString *className = NSStringFromClass([self class]);
if ([[NSObject classNamesWhitelist] containsObject:className])
return NO;
// 如果该对象被 UIApplication 对象引用这, 则也不会去检测泄露
NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;
__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;
[strongSelf assertNotDealloc];
});
return YES;
}
+ (NSMutableSet *)classNamesWhitelist {
static NSMutableSet *whitelist = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
whitelist = [NSMutableSet setWithObjects:
@"UIFieldEditor", // UIAlertControllerTextField
@"UINavigationBar",
@"_UIAlertControllerActionView",
@"_UIVisualEffectBackdropView",
nil];
// System's bug since iOS 10 and not fixed yet up to this ci.
NSString *systemVersion = [UIDevice currentDevice].systemVersion;
if ([systemVersion compare:@"10.0" options:NSNumericSearch] != NSOrderedAscending) {
[whitelist addObject:@"UISwitch"];
}
});
return whitelist;
}
5. 检测完当前对象后, 继续检测它的子view或者子控制器甚至其它属性
当完成对象自身的检测后, 还需要对它的附属进行检测. 比如 UIViewController
作为容器控制器时, 当它应该被销毁时, 则它的一些子控制器或者控制器的 view
等等也应该被销毁, 故都需要检测.
@implementation UIViewController (MemoryLeak)
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}
同理如果是 view
, 那么它的 subViews
也应该被检测
@implementation UIView (MemoryLeak)
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.subviews];
return YES;
}
同理如果是 UINavigationController
或者 UIPageViewController
或者 UISplitViewController
或者 UITabBarController
等容器控制器, 那么它的 viewControllers
也应该被检测
@implementation UINavigationController (MemoryLeak)
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
再提一点, 对于使用 Method Swizzling
交换两个方法, 较为谨慎的写法
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSEL);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
BOOL didAddMethod =
class_addMethod(class,
originalSEL,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 添加成功表明当前类添加之前并没有originalSEL这个方法(意味着你尝试替换一个并不存在的方法).
// 那么执行到此处.已经帮你给这个类添加了originalSEL这个方法.且实现是swizzledMethod.
// 此时再将当前类的swizzledSEL的实现替换为 method_getImplementation(originalMethod)
// 此时获取的实现已经是swizzledMethod了.
// 后续无论你调用 originalSEL 还是 swizzledSEL 都是同样的实现 swizzledMethod
class_replaceMethod(class,
swizzledSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
#endif
}