1. 开篇叨叨

在iOS内存优化总结那篇博客中提到过MLeaksFinder,并简要介绍了它的实现原理,这里主要从源码角度来看下MLeaksFinder是怎样做到内存泄漏检测的。

2. 代码结构与整体思路

我们先来看下整个源码的目录结构:

代码主要分成三类:

1. 各种对象的分类

NSObject+MemoryLeak.h
UIApplication+MemoryLeak.h
UINavigationController+MemoryLeak.h
UIPageViewController+MemoryLeak.h
UISplitViewController+MemoryLeak.h
UITabBarController+MemoryLeak.h
UITouch+MemoryLeak.h
UIView+MemoryLeak.h
UIViewController+MemoryLeak.h

这些分类主要完成触发对象回收方法的Hook,以及NSObject+MemoryLeak方法的覆盖。

2. MLeaksMessenger 发生泄漏时候的提醒执行对象

3. MLeakedObjectProxy 堆栈信息管理对象

再进一步往细地讲首先MemoryLeak分类会Hook对应触发回收的方法,一旦触发回收,就会递归地调用NSObject+MemoryLeak 中对应的willDealloc方法,再这个方法中会延迟2秒调用assertNotDealloc,如果这2秒内对象被销毁那么assertNotDealloc将不会被调用,否则将调用assertNotDealloc,在assertNotDealloc将会通过MLeakedObjectProxy构建泄漏堆栈,然后通过MLeaksMessenger发出泄漏提醒信息。

整个MLeaksFinder的代码量不是很大,顺着上面介绍的思路不会有太大理解上的问题:

3. 各个类的分类

首先我们看下Hook部分。我们先来看下每个分类都Hook了哪些方法:

  • NSObject+MemoryLeak 方法没有Hook任何方法,它只不过作为基类提供一些公共的方法。
  • UIApplication+MemoryLeak 方法,Hook了sendAction:to:from:forEvent:在这个方法中将sender存储起来,至于用来干啥用先卖个关子,后面介绍NSObject+MemoryLeak的时候介绍。
  • UINavigationController+MemoryLeak 主要是Hook了:
pushViewController:animated:
popViewControllerAnimated:
popToViewController:animated:
popToRootViewControllerAnimated:

它的主要工作是对每个pop出来对ViewController调用willDealloc

- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
  • UIPageViewController+MemoryLeak,UISplitViewController+MemoryLeak,UITabBarController+MemoryLeak,UIView+MemoryLeak

里面都只有一个方法,主要是对自己对子ViewController进行判断是否泄漏。

- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
  • UITouch+MemoryLeak中只Hook了setView:方法在这里将view保存为sender

  • UIViewController+MemoryLeak Hook了

viewDidDisappear:
viewWillAppear:
dismissViewControllerAnimated:completion:

并重写了:

- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];

if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}

在这里对self.presentedViewController ,self.childViewControllers,self.view检测是否有泄漏。

4. 情景分析

接下来我们以通过push进入一个界面到pop出来为情景看下MLeaksFinder是怎样检测内存泄漏的:

首先是push方法:

- (void)swizzled_pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.splitViewController) {
id detailViewController = objc_getAssociatedObject(self, kPoppedDetailVCKey);
if ([detailViewController isKindOfClass:[UIViewController class]]) {
[detailViewController willDealloc];
objc_setAssociatedObject(self, kPoppedDetailVCKey, nil, OBJC_ASSOCIATION_RETAIN);
}
}
[self swizzled_pushViewController:viewController animated:animated];
}

对于普通的viewController self.splitViewController 为 nil所以这里实际上是没做什么工作,我们直接略过。

这时候UIViewController 将会被push进来这时候会调用viewWillAppear,由于viewWillAppear被Hook了,所以实际上调用的是:

- (void)swizzled_viewWillAppear:(BOOL)animated {
[self swizzled_viewWillAppear:animated];
objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}

这里会将kHasBeenPoppedKey设置为NO,这个有什么作用我们往下看:

在我们pop UIViewController的时候会调用swizzled_popViewControllerAnimated

- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {
UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];

//.....

// VC is not dealloced until disappear when popped using a left-edge swipe gesture
extern const void *const kHasBeenPoppedKey;
objc_setAssociatedObject(poppedViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN);

return poppedViewController;
}

在swizzled_popViewControllerAnimated方法中只是将kHasBeenPoppedKey设置为YES,但是并没调用willDealloc,这是因为在使用左边缘滑动关闭的时候,要等到UIViewController disappear的时候才开始销毁,所以这里只是设置一个标记延迟调用willDealloc。

在然后UIViewController的swizzled_viewDidDisappear会被调用,由于上面已经设置kHasBeenPoppedKey为YES,所以willDealloc就会被调用。

- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
[self willDealloc];
}
}
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}

willDealloc 中会先调用NSObject+MemoryLeak的willDealloc

- (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;
}

NSObject+MemoryLeak的willDealloc 中会先判断当前类是否在白名单中。

+ (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;
}

也就是说UIFieldEditor,UINavigationBar,_UIAlertControllerActionView,_UIAlertControllerActionView以及10.0以后的UISwitch都在白名单中,如果是这些对象将不做检查。

下一步:

NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;

是否还记得在讲UIApplication+MemoryLeak中的Hook的时候有讲到UIApplication+MemoryLeak会将当前sender保存起来,这里就得提一下iOS 的 Target-Action机制了。当一个事件发生的时候,UIControl会调用sendAction:to:forEvent:将行为消息转发到UIApplication对象,再由UIApplication对象调用其sendAction:to:fromSender:forEvent:方法来将消息分发到指定的target上,而如果我们没有指定target,则会将事件分发到响应链上第一个想处理消息的对象上。而如果子类想监控或修改这种行为的话,则可以重写这个方法。在UIApplication+MemoryLeak将最近正在执行的sender存储起来就是为了在这个地方与self进行对比,也就是说如果当前对象正在执行action那么就不再对该对象进行内存检测。否则就会延迟两秒调用assertNotDealloc方法。

假设发生了内存泄漏,当前对象没有被释放,那么assertNotDealloc将会被调用:

- (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]);
}

在assertNotDealloc中将会检查是否已经在泄漏名单中了,如果已经在了就不再添加直接放回,如果不在那么就调用addLeakedObject将当前对象添加到泄漏名单。

+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs {
NSAssert([NSThread isMainThread], @"Must be in main thread.");
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
leakedObjectPtrs = [[NSMutableSet alloc] init];
});
if (!ptrs.count) {
return NO;
}
if ([leakedObjectPtrs intersectsSet:ptrs]) {
return YES;
} else {
return NO;
}
}

紧接着就会通过MLeaksMessenger 弹出弹窗提示用户泄漏堆栈:

+ (void)addLeakedObject:(id)object {
NSAssert([NSThread isMainThread], @"Must be in main thread.");

MLeakedObjectProxy *proxy = [[MLeakedObjectProxy alloc] init];
proxy.object = object;
proxy.objectPtr = @((uintptr_t)object);
proxy.viewStack = [object viewStack];
static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey;
objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);

[leakedObjectPtrs addObject:proxy.objectPtr];

#if _INTERNAL_MLF_RC_ENABLED
[MLeaksMessenger alertWithTitle:@"Memory Leak"
message:[NSString stringWithFormat:@"%@", proxy.viewStack]
delegate:proxy
additionalButtonTitle:@"Retain Cycle"];
#else
[MLeaksMessenger alertWithTitle:@"Memory Leak"
message:[NSString stringWithFormat:@"%@", proxy.viewStack]];
#endif
}

如果我们点击弹窗上的按钮查看具体的循环引用信息那么将会调用下面的方法:

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (!buttonIndex) {
return;
}

id object = self.object;
if (!object) {
return;
}

#if _INTERNAL_MLF_RC_ENABLED
dispatch_async(dispatch_get_global_queue(0, 0), ^{
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:self.object];
NSSet *retainCycles = [detector findRetainCyclesWithMaxCycleLength:20];

BOOL hasFound = NO;
for (NSArray *retainCycle in retainCycles) {
NSInteger index = 0;
for (FBObjectiveCGraphElement *element in retainCycle) {
if (element.object == object) {
NSArray *shiftedRetainCycle = [self shiftArray:retainCycle toIndex:index];

dispatch_async(dispatch_get_main_queue(), ^{
[MLeaksMessenger alertWithTitle:@"Retain Cycle"
message:[NSString stringWithFormat:@"%@", shiftedRetainCycle]];
});
hasFound = YES;
break;
}

++index;
}
if (hasFound) {
break;
}
}
if (!hasFound) {
dispatch_async(dispatch_get_main_queue(), ^{
[MLeaksMessenger alertWithTitle:@"Retain Cycle"
message:@"Fail to find a retain cycle"];
});
}
});
#endif
}

在alertView方法中对通过FBRetainCycleDetector来检测循环引用,然后通过弹窗进行展示,这部分将会在FBRetainCycleDetector源码解析中进行详细介绍。

除了对UIViewController 自身调用willDealloc还需要对当前UIViewController的self.presentedViewController,self.childViewControllers以及self.view调用willDealloc判断是否有泄漏发生。

- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}

如果某个对象被检测到2秒内还没被释放,但是在2秒之后还是调用了dealloc释放了,那么这种不算是内存泄漏,所以会弹出Object Deallocated提示该对象已经被释放了,不属于内存泄漏。

- (void)dealloc {
NSNumber *objectPtr = _objectPtr;
NSArray *viewStack = _viewStack;
dispatch_async(dispatch_get_main_queue(), ^{
[leakedObjectPtrs removeObject:objectPtr];
[MLeaksMessenger alertWithTitle:@"Object Deallocated"
message:[NSString stringWithFormat:@"%@", viewStack]];
});
}
Contents
  1. 1. 1. 开篇叨叨
  2. 2. 2. 代码结构与整体思路
  3. 3. 3. 各个类的分类
  4. 4. 4. 情景分析