为了方便理解, 跟源码有出入.
核心原理
- (BOOL)willDealloc {
__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;
}
在你认为某对象将要dealloc的地方,调用一下该对象的willDealloc, 如果2秒钟后该对象依旧存在,说明该对象可能释放不及时,存在内存泄漏
所以重要的是什么时候应该调用willDealloc呢?
具体操作
MLeaksFinder针对view和vc,帮我们在合适的时机自动调用了willDealloc
针对vc
vc什么时候应该调用willdealloc?
- 有
vc执行dismissViewControllerAnimated:completion:时
背景知识:
当我们vcA执行presentViewController:animated:completion:弹出vcB时
此时,
vcB.presentingViewController == vcA
vcA.presentedViewController == vcB
当我们的vc执行dismissViewControllerAnimated:completion:时
如果该vc有presentedViewController, 那么将自己的presentedViewController控制器dismiss掉
如果该vc没有presentedViewController, 那么就相当于让自己的presentingViewController执行dismissViewControllerAnimated:completion:
也就是说:
当某vc执行dismissViewControllerAnimated:completion:时
如果该vc有presentedViewController, 那么该vc的presentedViewController需要调用willdealloc
如果该vc没有presentedViewController, 那么该vc需要调用willdealloc
// UIViewController (MemoryLeak)
- (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];
}
- 当
vc的navigationController执行了popToRootViewControllerAnimated:
当所属的navigationController调用popToRootViewControllerAnimated:时,返回值是所有需要出栈的vc数组, vc出栈正常应该销毁,所以调用willdealloc
// UINavigationController (MemoryLeak)
- (NSArray<UIViewController *> *)swizzled_popToRootViewControllerAnimated:(BOOL)animated {
NSArray<UIViewController *> *poppedViewControllers = [self swizzled_popToRootViewControllerAnimated:animated];
for (UIViewController *viewController in poppedViewControllers) {
[viewController willDealloc];
}
return poppedViewControllers;
}
- 当
vc的navigationController执行了popToViewController:animated:
同2
// UINavigationController (MemoryLeak)
- (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;
}
- 当
vc的navigationController执行了popViewControllerAnimated:这个稍微有点不同, 这个是因为有侧滑手势触发时, vc的navigationController就会触发popViewControllerAnimated:, 但是侧滑手势触发后并不是一定会把该vc出栈, 所以该vc不能在popViewControllerAnimated:时调用willDealloc
MLeaksFinder是这么做的,在pop时, 设置一个标志位来标志是否被pop过, 在viewDidDisAppear判断是否被pop过, 如果被该vc被pop过,并且调用了viewDidDisAppear,说明这个vc是真实出栈了, 当然下一次重新appear的时候, 需要将标志位恢复
// UIViewController (MemoryLeak)
- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
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);
}
// UINavigationController (MemoryLeak)
- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {
UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];
if (!poppedViewController) {
return nil;
}
// 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;
}
vc的willdealloc
vc调用willdealloc时
- 其
childViewControllers中的所有子vc和presentedViewController都需要调用willdealloc, 如果该vc有viewControllers属性, 例如UINavigationController,UIPageViewController,UITabBarController等, 其viewControllers中的vc也需要调用willdealloc - 其
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;
}
针对view
- 每一个
vc在willDealloc时需要将自己的view调用willdealloc - 每一个的
view在willDealloc时需要将自己的subviews调用willdealloc
// UIView (MemoryLeak)
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.subviews];
return YES;
}
视图栈(定位页面)
视图栈的作用是为更准确的知道发生内存泄露的vc和view所处的位置
由对象A触发了对象B的willDealloc, 那么
应令对象B视图栈 = 对象A的视图栈 + 对象B的类
这样对象B就能顺着视图栈找到最顶层的调用willDealloc的vc来准确判断位置
// NSObject (MemoryLeak)
- (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 ];
}
- (void)setViewStack:(NSArray *)viewStack {
objc_setAssociatedObject(self, kViewStackKey, viewStack, OBJC_ASSOCIATION_RETAIN);
}
地址栈(防止无用上报和重报)
举个例子:
对象A已经被发现有内存泄漏
而由对象A触发了对象B的willDealloc, 这时候对象B的内存泄漏需要提示吗?
这种情况是不需要的, 对象A发生内存泄漏后, 由它触发willDealloc的对象大概率也会因为它的内存泄漏而导致内存泄漏
所以, 我们当发现对象A内存泄漏后, 另一个内存泄漏的对象需要判断一下其地址栈上是否存在对象A, 如果存在, 则不需要报告
地址栈, 其生成逻辑跟视图栈是一样的, 视图栈上存的是类名, 地址栈中存的是对象地址
// NSObject (MemoryLeak)
- (void)assertNotDealloc {
if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
return;
}
[MLeakedObjectProxy addLeakedObject:self];
}
// MLeakedObjectProxy
+ (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.");
[leakedObjectPtrs addObject:@((uintptr_t)object)];
// 可在此处报告内存泄漏
}
其他小技巧
- 支持某些类加入白名单, 代表这些类下的对象不需要内存监测
// NSObject (MemoryLeak)
+ (void)addClassNamesToWhitelist:(NSArray *)classNames;
- 可以自动手动调用
willDealloc, 解决类似这样的情况, 一个单例对象, 但是其某些属性对象需要在合适的时机释放, 这时候可以让这个单例对象手动调用willReleaseObject:relationship:来触发这些属性对象的willDealloc, 这个方法有个宏MLCheck
// NSObject (MemoryLeak)
- (void)willReleaseObject:(id)object relationship:(NSString *)relationship {
if ([relationship hasPrefix:@"self"]) {
relationship = [relationship stringByReplacingCharactersInRange:NSMakeRange(0, 4) withString:@""];
}
NSString *className = NSStringFromClass([object class]);
className = [NSString stringWithFormat:@"%@(%@), ", relationship, className];
[object setViewStack:[[self viewStack] arrayByAddingObject:className]];
[object setParentPtrs:[[self parentPtrs] setByAddingObject:@((uintptr_t)object)]];
[object willDealloc];
}
#define MLCheck(TARGET) [self willReleaseObject:(TARGET) relationship:@#TARGET];
- 当然某些情况下,可能某些对象在3秒钟后才释放, 并不存在内存泄漏, 但是
MLeaksFinder在2秒钟后就会报告内存泄漏, 这时候可能存在误报, 针对这种情况,MLeaksFinder在对象正式释放后, 也会有dealloced提示.
3.1 在地址栈中,MLeakedObjectProxy在addLeakedObject的时候,新建了一个MLeakedObjectProxy对象跟泄漏的object用强引用策略绑定起来.这样只有object释放的时候,MLeakedObjectProxy对象才会释放
3.2 这个MLeakedObjectProxy对象调用dealloc的时候, 代表object已经释放了, 这时候需要提示可能误报了, 这个对象是能正常释放的
// MLeakedObjectProxy
+ (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;
// 绑定, 强引用, 只有`object`释放的时候, proxy才会释放
objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);
[leakedObjectPtrs addObject:proxy.objectPtr];
}
- (void)dealloc {
NSNumber *objectPtr = _objectPtr;
NSArray *viewStack = _viewStack;
dispatch_async(dispatch_get_main_queue(), ^{
[leakedObjectPtrs removeObject:objectPtr];
// 可在此处报告有误报情况, 可以把视图栈带上方便定位位置
});
}
小优化
- 针对同一个类下, 部分对象可能不需要监测内存泄漏的情况支持的不好, 可以通过加个关联属性解决
NavigationController的setViewControlls:方法没有做处理