检测内存泄漏一般有以下几种方式。
- 通过dealloc判断
- Instruments -> leak
- Debug Memory Graph
- MLeaksFinder
通过打印dealloc的方式,需要每个页面都仔细检查,并且许多泄漏是在很多处理的前提才会出现的,所以该方式需要大量时间去排查。
Instruments -> leak的方式适合大范围,查找全部的泄漏点,数据量大。
Debug Memory Graph是会通过图形化的方式显示引用的对象。
MLeaksFinder是通过hook来实现无代码入侵的自动检测对象是否正常释放。
MLeaksFinder最主要的实现是通过对象将要释放时,延迟执行一个block。block内部通过self去调用一个方法。如果还能调用,说明该对象还未释放,并且记录视图的栈通过alert显示出来提醒开发者。
MLeaksFinder通过实现了几个重要类的分类(UINavigationController、UITabBarController、UIViewController、UIView、NSObject),替换部视图的生命周期方法,以便检测和处理。
通过几个主要的类看内部实现
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:)];
});
}
// 离开页面
- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
// 当nav pop出时才去执行willDealloc才有意义
// 配合UINavigationController+MemoryLeak使用
if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
[self willDealloc];
}
}
// 进入页面
- (void)swizzled_viewWillAppear:(BOOL)animated {
[self swizzled_viewWillAppear:animated];
objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}
// dismiss时的释放检测
- (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];
}
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
// 递归排查所有的子控制器
[self willReleaseChildren:self.childViewControllers];
// 检查present的控制器
[self willReleaseChild:self.presentedViewController];
// 是否正在显示 递归view的全部子view
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}
UIView+MemoryLeak
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
// 递归检查所有子view
[self willReleaseChildren:self.subviews];
return YES;
}
NSObject+MemoryLeak
- (BOOL)willDealloc {
NSString *className = NSStringFromClass([self class]);
// 是否在白名单内
if ([[NSObject classNamesWhitelist] containsObject:className])
return NO;
// 检查最后一个发送action的是不是当前对象 是的话就不处理,因为willDealloc会先调用
NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;
// 检测内存泄漏的核心实现
// 延迟2秒后执行self的方法 如果不为nil则会执行 说明未释放
__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 {
// 判断当前泄漏的Set是不是已经在泄漏名单里面了
if ([LeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
return;
}
// 加入名单内
[LeakedObjectProxy 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]);
}
// 检查泄漏
- (void)willReleaseChild:(id)child {
if (!child) {
return;
}
[self willReleaseChildren:@[ child ]];
}
// 检查所有数组内的对象的泄漏
- (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];
}
}
// 获取当前路径栈
- (NSArray *)viewStack {
NSArray *viewStack = objc_getAssociatedObject(self, kViewStackKey);
if (viewStack) {
return viewStack;
}
NSString *className = NSStringFromClass([self class]);
return @[ className ];
}
// 设置view的路径栈
- (void)setViewStack:(NSArray *)viewStack {
objc_setAssociatedObject(self, kViewStackKey, viewStack, OBJC_ASSOCIATION_RETAIN);
}
// 获取路径上的所有对象
- (NSSet *)parentPtrs {
NSSet *parentPtrs = objc_getAssociatedObject(self, kParentPtrsKey);
if (!parentPtrs) {
parentPtrs = [[NSSet alloc] initWithObjects:@((uintptr_t)self), nil];
}
return parentPtrs;
}
// 设置路径上的对象 用set保存就可以 用于判断
- (void)setParentPtrs:(NSSet *)parentPtrs {
objc_setAssociatedObject(self, kParentPtrsKey, parentPtrs, OBJC_ASSOCIATION_RETAIN);
}
// 白名单
+ (NSMutableSet *)classNamesWhitelist {
static NSMutableSet *whitelist = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
whitelist = [NSMutableSet setWithObjects:
@"UIFieldEditor",
@"UINavigationBar",
@"_UIAlertControllerActionView",
@"_UIVisualEffectBackdropView",
nil];
});
return whitelist;
}
// 动态添加至白名单
+ (void)addClassNamesToWhitelist:(NSArray *)classNames {
[[self classNamesWhitelist] addObjectsFromArray:classNames];
}
// 交换方法
+ (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) {
class_replaceMethod(class,
swizzledSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
LeakedObjectProxy
// 通过交集判断是否已经存在
+ (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;
}
}
// 添加对象到泄漏名单内
+ (void)addLeakedObject:(id)object {
NSAssert([NSThread isMainThread], @"Must be in main thread.");
LeakedObjectProxy *proxy = [[LeakedObjectProxy 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];
// 弹窗提醒
[self alertWithTitle:@"内存泄漏" message:[NSString stringWithFormat:@"%@", proxy.viewStack]];
}
// 弹窗显示
+ (void)alertWithTitle:(NSString *)title message:(NSString *)message {
dispatch_block_t block = ^() {
UIAlertController *temp = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
[temp addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:temp animated:YES completion:nil];
alertController = temp;
};
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"程序捕捉到内存泄漏警告: title=%@, message=%@", title, message);
if (alertController) {
[alertController dismissViewControllerAnimated:NO completion:block];
}
else {
block();
}
});
}
简单的一个控制器在dismiss后,检查路径如下
- swizzled_dismissViewControllerAnimated中执行willDealloc
- 递归当前对象的所有子控制器、persent控制器
- 递归检查self.view
- UIView的willDealloc会检查所有子View
- NSObject中willDealloc 判断检查的对象是否在白名单中
- 添加延时2秒的方法
- 2秒后,如果存在泄漏的对象将其添加到泄漏名单中
- 将泄漏对象的视图栈中所有的类名显示在弹窗