IOS內(nèi)存泄漏檢查方法及重寫MLeakFinder
對于iOS開發(fā)來講,內(nèi)存泄漏的問題,已經(jīng)是老生常談的話題。在日常的面試中經(jīng)常會(huì)提到這些問題。我們?nèi)粘5拈_發(fā)過程中進(jìn)行內(nèi)存泄漏的檢測,一般是使用instrument工具中的Leaks/Allocation來進(jìn)行排查,網(wǎng)絡(luò)上也有比較高效又好用的內(nèi)存泄漏檢測工具,MLeakFinder。
MLeakFinder-原理
首先看UIViewController,當(dāng)一個(gè)UIViewController被pop或dismiss的時(shí)候,這個(gè)VC包括在這個(gè)VC上的View,或者子View都會(huì)很快的被釋放。所以我們我們需要在UIViewController被POP或dismiss后一小段時(shí)間后,在這個(gè)VC上的view,subView等是否還存在。
在UIViewController+MemoryLeak.h的load方法中可以看到,早+load方法中通過runtime交換了viewWillAppear,viewDidAppear,dismissViewControllerAnimated:completion:這三個(gè)方法。
1,首先看viewWillAppear
- (void)swizzled_viewWillAppear:(BOOL)animated {
[self swizzled_viewWillAppear:animated];
objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}
當(dāng)VC進(jìn)來的時(shí)候,添加關(guān)聯(lián)對象,并標(biāo)記為NO
2,在看viewDidAppear
- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
[self willDealloc];
}
}
通過代碼可以看出,獲取當(dāng)前關(guān)聯(lián)對象的標(biāo)記,當(dāng)標(biāo)記為YES的時(shí)候,就會(huì)調(diào)用willDealloc。
3,我們看什么時(shí)候會(huì)被標(biāo)記為YES呢?
在UINavigationController+MemoryLeak.h的popViewControllerAnimated:方法中我們可以看到
- (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或者左滑返回的時(shí)候,相當(dāng)于視圖銷毀,就會(huì)被標(biāo)記為YES。
4,我們重點(diǎn)看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;
}
1.第一步:我們可以看到,會(huì)先判斷當(dāng)前的class是否在白名單中,是的話就會(huì)return NO,即不是內(nèi)存泄漏的。
同時(shí)我們查看構(gòu)建白名單的源碼:使用了一個(gè)單例實(shí)現(xiàn),確保只有一個(gè),是個(gè)私有方法
+ (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;
}
同時(shí)在還支持,自定義的添加白名單
+ (void)addClassNamesToWhitelist:(NSArray *)classNames {
[[self classNamesWhitelist] addObjectsFromArray:classNames];
}
2. 第二步:判斷該對象是否是上一次發(fā)送action的對象,是的話,不進(jìn)行內(nèi)存檢測
//第二步
NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey)
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;
3,第三步:弱指針指向self,2s延遲,然后通過這個(gè)弱指針調(diào)用-assertNotDealloc,若被釋放,給nil發(fā)消息直接返回,不觸發(fā)-assertNotDealloc方法,認(rèn)為已經(jīng)釋放;如果它沒有被釋放(泄漏了),-assertNotDealloc就會(huì)被調(diào)用
__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,現(xiàn)在我們回到:2的代碼 [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;
}
1,第一步:會(huì)通過 super調(diào)用父類的willDealloc,即上面目錄4
2,第二步:調(diào)用willReleaseChildren,willReleaseChild遍歷該對象的子對象,看其是否釋放
- (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];
}
}
通過代碼可以看出,通過調(diào)用willReleaseChildren的方法,獲取當(dāng)前對象viewStack,parentPtrs,并且遍歷children,為每個(gè)子對象設(shè)置viewStack,parentPtrs,然后調(diào)用willDealloc。
通過源碼看一下viewStask,parentPtrs的實(shí)現(xiàn)
- (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使用數(shù)組,parentPtrs使用的集合形式。都是通過運(yùn)行時(shí),用關(guān)聯(lián)對象添加屬性。
parentPtrs會(huì)在-assertNotDealloc中,會(huì)判斷當(dāng)前對象是否與父節(jié)點(diǎn)集合有交集。下面仔細(xì)看下-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]);
}
1,第一步我們看到,通過parentPtrs的判斷是否有交集
產(chǎn)看其源碼:
+ (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;
}}
可以看到,創(chuàng)建了一個(gè)單例對象,通過集合的形式,判斷是否有交集,是的話return。否則就進(jìn)入第二步
2,第二步:addLeakedObject
+ (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
}
第一步:構(gòu)造MLeakedObjectProxy對象,給傳入的泄漏對象 object 關(guān)聯(lián)一個(gè)代理即 proxy
第二步:通過objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN)方法,object強(qiáng)持有proxy, proxy若持有object,如果object釋放,proxy也會(huì)釋放
第三步:存儲(chǔ) proxy.objectPtr(實(shí)際是對象地址)到集合 leakedObjectPtrs 里邊
第四步:彈框 AlertView若 _INTERNAL_MLF_RC_ENABLED == 1,則彈框會(huì)增加檢測循環(huán)引用的選項(xiàng);若 _INTERNAL_MLF_RC_ENABLED == 0,則僅展示堆棧信息。
對于MLeakedObjectProxy類而言,是檢測到內(nèi)存泄漏才產(chǎn)生的,作為泄漏對象的屬性存在的,如果泄漏的對象被釋放,那么MLeakedObjectProxy也會(huì)被釋放,則調(diào)用-dealloc函數(shù)
集合leakedObjectPtrs中移除該對象地址,同時(shí)再次彈窗,提示該對象已經(jīng)釋放了
6,自己也在嘗試重寫該框架,歡迎大家一起交流
以上就是IOS內(nèi)存泄漏檢查方法及重寫MLeakFinder的詳細(xì)內(nèi)容,更多關(guān)于IOS內(nèi)存泄漏的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解IOS11新特性之larget title的實(shí)現(xiàn)
本篇文章主要介紹了詳解IOS11新特性之larget title的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-12-12
iOS實(shí)現(xiàn)多個(gè)垂直滑動(dòng)條并列視圖
這篇文章主要為大家詳細(xì)介紹了iOS實(shí)現(xiàn)多個(gè)垂直滑動(dòng)條并列視圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
IOS用AFN發(fā)送字符串形式的Json數(shù)據(jù)給服務(wù)器實(shí)例
本篇文章主要介紹了IOS用AFN發(fā)送字符串形式的Json數(shù)據(jù)給服務(wù)器實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04
iOS中的NSURLCache數(shù)據(jù)緩存類用法解析
iOS App中具體緩存操作的管理我們通常是用NSURLCache類來實(shí)現(xiàn)的,下面我們就來看一下iOS中的NSURLCache數(shù)據(jù)緩存類用法解析:2016-06-06
iOS overFullScreen與fullScreen區(qū)別分析
這篇文章主要介紹了iOS overFullScreen與fullScreen區(qū)別分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
深入詳解Objective-C中的@Synchronized關(guān)鍵字
這篇文章主要為大家介紹了深入詳解Objective-C中的@Synchronized關(guān)鍵字,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
判斷iOS應(yīng)用是否開放HTTP權(quán)限的方法
這篇文章主要為大家詳細(xì)介紹了判斷iOS應(yīng)用是否開放HTTP權(quán)限的方法,感興趣的小伙伴們可以參考一下2016-03-03

