对于iOS开发来讲,内存泄漏的问题,已经是老生常谈的话题。在日常的面试中经常会提到这些问题。我们日常的开发过程中进行内存泄漏的检测,一般是使用instrument工具中的Leaks/Allocation来进行排查,网络上也有比较高效又好用的内存泄漏检测工具,MLeakFinder。
MLeakFinder-原理
首先看UIViewController,当一个UIViewController被pop或dismiss的时候,这个VC包括在这个VC上的View,或者子View都会很快的被释放。所以我们我们需要在UIViewController被POP或dismiss后一小段时间后,在这个VC上的view,subView等是否还存在。
在UIViewController+MemoryLeak.h的load方法中可以看到,早+load方法中通过runtime交换了viewWillAppear,viewDidAppear,dismissViewControllerAnimated:completion:这三个方法。
1,首先看viewWillAppear
1
2
3
4
|
- ( void )swizzled_viewWillAppear:( BOOL )animated {
[self swizzled_viewWillAppear:animated];
objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}
|
当VC进来的时候,添加关联对象,并标记为NO
2,在看viewDidAppear
1
2
3
4
5
6
|
- ( void )swizzled_viewDidDisappear:( BOOL )animated {
[self swizzled_viewDidDisappear:animated];
if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
[self willDealloc];
}
}
|
通过代码可以看出,获取当前关联对象的标记,当标记为YES的时候,就会调用willDealloc。
3,我们看什么时候会被标记为YES呢?
在UINavigationController+MemoryLeak.h的popViewControllerAnimated:方法中我们可以看到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
- (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;
}
|
我们可以看出,在VC被pop或者左滑返回的时候,相当于视图销毁,就会被标记为YES。
4,我们重点看willDealloc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
- ( 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;
}
|
1.第一步:我们可以看到,会先判断当前的class是否在白名单中,是的话就会return NO,即不是内存泄漏的。
同时我们查看构建白名单的源码:使用了一个单例实现,确保只有一个,是个私有方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
+ (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;
}
|
同时在还支持,自定义的添加白名单
1
2
3
|
+ ( void )addClassNamesToWhitelist:(NSArray *)classNames {
[[self classNamesWhitelist] addObjectsFromArray:classNames];
}
|
2. 第二步:判断该对象是否是上一次发送action的对象,是的话,不进行内存检测
1
2
3
4
5
6
7
|
//第二步
NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey)
if ([senderPtr isEqualToNumber:@(( uintptr_t )self)])
return NO;
|
3,第三步:弱指针指向self,2s延迟,然后通过这个弱指针调用-assertNotDealloc,若被释放,给nil发消息直接返回,不触发-assertNotDealloc方法,认为已经释放;如果它没有被释放(泄漏了),-assertNotDealloc就会被调用
1
2
3
4
5
|
__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];
});
|
5,现在我们回到:2的代码 [self willDealloc]
看一下他的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
|
- ( BOOL )willDealloc {
//第一步
if (![super willDealloc]) {
return NO;
}
//第二步
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}
|
1,第一步:会通过 super调用父类的willDealloc,即上面目录4
2,第二步:调用willReleaseChildren,willReleaseChild遍历该对象的子对象,看其是否释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
- ( 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];
}
}
|
通过代码可以看出,通过调用willReleaseChildren的方法,获取当前对象viewStack,parentPtrs,并且遍历children,为每个子对象设置viewStack,parentPtrs,然后调用willDealloc。
通过源码看一下viewStask,parentPtrs的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
- (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);
}
- (NSSet *)parentPtrs {
NSSet *parentPtrs = objc_getAssociatedObject(self, kParentPtrsKey);
if (!parentPtrs) {
parentPtrs = [[NSSet alloc] initWithObjects:@(( uintptr_t )self), nil];
}
return parentPtrs;
}
- ( void )setParentPtrs:(NSSet *)parentPtrs {
objc_setAssociatedObject(self, kParentPtrsKey, parentPtrs, OBJC_ASSOCIATION_RETAIN);
}
|
viewStack使用数组,parentPtrs使用的集合形式。都是通过运行时,用关联对象添加属性。
parentPtrs会在-assertNotDealloc中,会判断当前对象是否与父节点集合有交集。下面仔细看下-assertNotDealloc方法
1
2
3
4
5
6
7
8
9
|
- ( 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]);
}
|
1,第一步我们看到,通过parentPtrs的判断是否有交集
产看其源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
+ ( 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;
}}
|
可以看到,创建了一个单例对象,通过集合的形式,判断是否有交集,是的话return。否则就进入第二步
2,第二步:addLeakedObject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
+ ( 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
}
|
第一步:构造MLeakedObjectProxy对象,给传入的泄漏对象 object 关联一个代理即 proxy
第二步:通过objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN)方法,object强持有proxy, proxy若持有object,如果object释放,proxy也会释放
第三步:存储 proxy.objectPtr(实际是对象地址)到集合 leakedObjectPtrs 里边
第四步:弹框 AlertView若 _INTERNAL_MLF_RC_ENABLED == 1,则弹框会增加检测循环引用的选项;若 _INTERNAL_MLF_RC_ENABLED == 0,则仅展示堆栈信息。
对于MLeakedObjectProxy类而言,是检测到内存泄漏才产生的,作为泄漏对象的属性存在的,如果泄漏的对象被释放,那么MLeakedObjectProxy也会被释放,则调用-dealloc函数
集合leakedObjectPtrs中移除该对象地址,同时再次弹窗,提示该对象已经释放了
6,自己也在尝试重写该框架,欢迎大家一起交流
以上就是IOS内存泄漏检查方法及重写MLeakFinder的详细内容,更多关于IOS内存泄漏的资料请关注服务器之家其它相关文章!
原文链接:https://juejin.cn/post/6954726462544936973