APM – iOS 内存泄漏监控 MLeaksFinder代码解析
简介
MLeaksFinder是微信开源的能够在iOS开发阶段用来检测内存走漏的库,能够主动的在UIView和UIViewController中发现走漏,经过弹出框显示在View-ViewController中的走漏目标。除了发现UIView和UIViewController的走漏目标,开发者也能够扩展到其他类型的目标。
并且能够经过FBRetainCycleDetector找到引证环。
内存分类
- Leaked memory 不被应用引证的内存,无法再次运用或者开释(能够运用Leaks Instrument东西检测到)
- Abandoned memory 没有用途的,仍被应用引证的内存
- Cached memory 仍然被应用引证,能够被再次运用然后得到更好的功能
原理
除了单例及其持有它的强引证,当一个UIViewController被pop或dismiss后,对应的View,View的subViews等目标将很快被开释。
Hook
Hook所有页面退出的状况作为检测机遇
-
UIViewController
- -dismiss
- -viewDidDisappear
-
UINavigationController
- -pop
- -popToRoot
- -popToViewController
视图层级
页面生命周期
UINavigationController -push/pop
-[MyUINavigationController pushViewController:animated:] -[ParentViewController viewWillDisappear:] -[ChildViewController viewWillAppear:] -[ParentViewController viewDidDisappear:] -[ChildViewController viewDidAppear:]
-[MyUINavigationController popViewControllerAnimated:] -[ChildViewController viewWillDisappear:] -[ParentViewController viewWillAppear:] -[ChildViewController viewDidDisappear:] -[ParentViewController viewDidAppear:] -[ChildViewController dealloc]
从上述的页面生命周期能够看到,viewDidDisappear在页面做push的时分也会触发,所以不能独自以viewDidDisappear作为检测机遇,需要以先有popViewControllerAnimated,后有viewDidDisappear为特征,才干作为页面出栈的判别。
Left-edge swipe
UINavigationController -pop有一种特殊状况是左滑手势
先左滑-后松开
-[MyUINavigationController popViewControllerAnimated:]
-[ChildViewController viewWillDisappear:]
-[ParentViewController viewWillAppear:]
-[ParentViewController viewWillDisappear:]
-[ParentViewController viewDidDisappear:]
-[ChildViewController viewWillAppear:]
-[ChildViewController viewDidAppear:]
先左滑-后完成
-[MyUINavigationController popViewControllerAnimated:]
-[ChildViewController viewWillDisappear:]
-[ParentViewController viewWillAppear:]
-[ChildViewController viewDidDisappear:]
-[ParentViewController viewDidAppear:]
-[ChildViewController dealloc]
因为左滑开端的时分,popViewControllerAnimated已经触发,并且时长由用户决定。因为检测需要运用延迟履行,查看页面等目标是否开释,所以不能独自以
popViewControllerAnimated为检测机遇,需要以先有popViewControllerAnimated,后有viewDidDisappear为特征,才干作为页面出栈的判别。
popToRoot/popToViewController
-[MyUINavigationController pushViewController:animated:]
-[MyUINavigationController pushViewController:animated:]
-[ParentViewController viewWillDisappear:]
-[OtherViewController viewWillAppear:]
-[ParentViewController viewDidDisappear:]
-[OtherViewController viewDidAppear:]
-[OtherViewController viewWillDisappear:]
-[ChildViewController viewWillAppear:]
-[OtherViewController viewDidDisappear:]
-[ChildViewController viewDidAppear:]
-[MyUINavigationController popToRootViewControllerAnimated:]
-[ChildViewController viewWillDisappear:]
-[ParentViewController viewWillAppear:]
-[OtherViewController dealloc]
-[ChildViewController viewDidDisappear:]
-[ParentViewController viewDidAppear:]
-[ChildViewController dealloc]
从页面生命周期能够看到,除了TopViewController和RootViewController/ToViewController,其他处于中心的ViewController在弹出页面栈的时分不会有生命周期的调用,能够获取中心的ViewController开端检测。
UIViewController -present/dismiss
Modal
Modal是指模态,形式状况,模态是人机交互过程中的一种状况,表现为用户相同的操作下能够发生不同的成果。例如使翻开文件夹时,点击和右击会触发不同的作用。弹框也有对应的对话框,弹出框,全屏框等作用。
根据Modal的不同,ParentViewController的Appear状况会不同,当ParentViewController完全被遮挡的时分,会有Appear相关的页面生命周期改变。
不同模态下的页面生命周期不同,能够分为3类。
半掩盖
- UIModalPresentationPageSheet
- UIModalPresentationFormSheet
- UIModalPresentationAutomatic
- UIModalPresentationCustom
- UIModalPresentationPopover
-[ParentViewController presentViewController:animated:completion:] -[ChildViewController viewWillAppear:] -[ChildViewController viewDidAppear:]
-[ChildViewController dismissViewControllerAnimated:completion:] -[ChildViewController viewWillDisappear:] -[ChildViewController viewDidDisappear:] -[ChildViewController dealloc]
全掩盖
- UIModalPresentationCurrentContext
- UIModalPresentationFullScreen
-[ParentViewController presentViewController:animated:completion:] -[ParentViewController viewWillDisappear:] -[ChildViewController viewWillAppear:] -[ChildViewController viewDidAppear:] -[ParentViewController viewDidDisappear:]
-[ChildViewController dismissViewControllerAnimated:completion:] -[ChildViewController viewWillDisappear:] -[ParentViewController viewWillAppear:] -[ParentViewController viewDidAppear:] -[ChildViewController viewDidDisappear:] -[ChildViewController dealloc]
报错
- UIModalPresentationOverFullScreen
- UIModalPresentationNone
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The specified modal presentation style doesn't have a corresponding presentation controller.'
这两种模态,会导致报OC语言层面反常,模态展现方式和对应controller不匹配
代码解析
运用了Method Swizzle对页面的生命周期进行了Hook
Method Swizzle
NSObject (MemoryLeak)中对swizzle做了封装
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
#if _INTERNAL_MLF_ENABLED
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) {
class_replaceMethod(class,
swizzledSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
#endif
}
运用宏作为功能开关,DEBUG的状况下翻开,或者手动设置MEMORY_LEAKS_FINDER_ENABLED为1翻开
//#define MEMORY_LEAKS_FINDER_ENABLED 0
#ifdef MEMORY_LEAKS_FINDER_ENABLED
#define _INTERNAL_MLF_ENABLED MEMORY_LEAKS_FINDER_ENABLED
#else
#define _INTERNAL_MLF_ENABLED DEBUG
#endif
UINavigationController (MemoryLeak)
popViewControllerAnimated
在该生命周期中符号kHasBeenPoppedKey为YES
- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {
UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];
if (!poppedViewController) {
return nil;
}
// Detail VC in UISplitViewController is not dealloced until another detail VC is shown
if (self.splitViewController &&
self.splitViewController.viewControllers.firstObject == self &&
self.splitViewController == poppedViewController.splitViewController) {
objc_setAssociatedObject(self, kPoppedDetailVCKey, poppedViewController, OBJC_ASSOCIATION_RETAIN);
return poppedViewController;
}
// 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;
}
在UIViewController的viewWillAppear中,符号kHasBeenPoppedKey为NO
- (void)swizzled_viewWillAppear:(BOOL)animated {
[self swizzled_viewWillAppear:animated];
objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}
popToViewController
获取中心的ViewController开端检测
- (NSArray<UIViewController *> *)swizzled_popToViewController:(UIViewController *)viewController animated:(BOOL)animated {
NSArray<UIViewController *> *poppedViewControllers = [self swizzled_popToViewController:viewController animated:animated];
for (UIViewController *viewController in poppedViewControllers) {
[viewController willDealloc];
}
return poppedViewControllers;
}
popToRootViewControllerAnimated
获取中心的ViewController开端检测
- (NSArray<UIViewController *> *)swizzled_popToRootViewControllerAnimated:(BOOL)animated {
NSArray<UIViewController *> *poppedViewControllers = [self swizzled_popToRootViewControllerAnimated:animated];
for (UIViewController *viewController in poppedViewControllers) {
[viewController willDealloc];
}
return poppedViewControllers;
}
UIViewController (MemoryLeak)
viewDidDisappear
判别kHasBeenPoppedKey为YES,然后开端检测
- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
[self willDealloc];
}
}
dismissViewControllerAnimated
- (void)swizzled_dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
[self swizzled_dismissViewControllerAnimated:flag completion:completion];
UIViewController *dismissedViewController = self.presentedViewController;
if (!dismissedViewController && self.presentingViewController) {
dismissedViewController = self;
}
if (!dismissedViewController) return;
[dismissedViewController willDealloc];
}
对应的presentedViewController,presentingViewController,self对应如下,获取到dismissedViewController并履行检测。因为dismissedViewController并没有边缘手势的影响,不需要运用dimiss和viewDidDisappear叠加的方式来判别检测的机遇。
-[ChildViewController dismissViewControllerAnimated:completion:]
self.presentedViewController (null)
self.presentingViewController <MyUINavigationController: 0x10181d600>
self <ChildViewController: 0x102d043e0>
-[ChildViewController viewWillDisappear:]
-[ChildViewController viewDidDisappear:]
-[ChildViewController dealloc]
Action
获取对应类的weakSelf,延迟2秒在主行列履行,调用weakSelf中的assertNotDealloc办法,办法还能呼应的话就记载相关信息
类结构
NSObject
-
UIViewController
- UINavigationController
- UIPageViewController
- UISplitViewController
- UITabBarController
-
UIView
检测目标中存在承继联系,检测办法也运用了承继联系
代码解析
NSObject (MemoryLeak)
willDealloc
- 如果在白名单中,不进行检测
- 如果走漏目标是最近一次UIControl事件的触发者,不进行检测(按照修改记载来看,当运用Button来pop ViewController的时分,VC不能及时开释)
- (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;
}
UIApplication (MemoryLeak)
- 获取和更新LatestSender
- (BOOL)swizzled_sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event {
objc_setAssociatedObject(self, kLatestSenderKey, @((uintptr_t)sender), OBJC_ASSOCIATION_RETAIN);
return [self swizzled_sendAction:action to:target from:sender forEvent:event];
}
UITouch (MemoryLeak)
- 获取和更新LatestSender
- (void)swizzled_setView:(UIView *)view {
[self swizzled_setView:view];
if (view) {
objc_setAssociatedObject([UIApplication sharedApplication],
kLatestSenderKey,
@((uintptr_t)view),
OBJC_ASSOCIATION_RETAIN);
}
}
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]);
}
willReleaseChildren
- 遍历视图树,构建视图堆栈ViewStack
- 记载父节点
- (void)willReleaseChildren:(NSArray *)children {
NSArray *viewStack = [self viewStack];
NSSet *parentPtrs = [self parentPtrs];
for (id child in children) {
NSString *className = NSStringFromClass([child class]);
[child setViewStack:[viewStack arrayByAddingObject:className]];
[child setParentPtrs:[parentPtrs setByAddingObject:@((uintptr_t)child)]];
[child willDealloc];
}
}
UINavigationController (MemoryLeak)
willDealloc
- 检测self.viewControllers
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
UIPageViewController (MemoryLeak)
willDealloc
- 检测self.viewControllers
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
UISplitViewController (MemoryLeak)
willDealloc
- 检测self.viewControllers
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
UITabBarController (MemoryLeak)
willDealloc
- 检测self.viewControllers
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
UIViewController (MemoryLeak)
willDealloc
- 检测self.childViewControllers
- 检测self.presentedViewController
- viewDidLoad的状况下从self.view开端遍历subviews
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}
UIView (MemoryLeak)
willDealloc
- 遍历view的subviews
UIView (MemoryLeak)
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.subviews];
return YES;
}
引证
MLeaksFinder Github
MLeaksFinder 新特性
MLeaksFinder:精准 iOS 内存走漏检测东西