為什么要寫這篇文章
之前一哥們兒去面試,被問(wèn)到檢測(cè)內(nèi)存泄漏的方式,然后他說(shuō)可以用instruments來(lái)調(diào)試來(lái)檢測(cè).面試官問(wèn)還有其他方式么,他說(shuō)可以用MLeaksFinder來(lái)進(jìn)行檢測(cè),然后面試官繼續(xù)問(wèn)知道原理么?后面就不提了.這里講解了MLeaksFinder原理.這激發(fā)了我的好奇心,然后花了些時(shí)間去研究這個(gè)框架的實(shí)現(xiàn)原理,同時(shí)令人驚喜的是,在找尋資料的同時(shí)發(fā)現(xiàn)了MRPeak的這篇文章也是講解如何實(shí)現(xiàn)一個(gè)內(nèi)存泄露檢測(cè)工具的,而且感覺(jué)更容易理解和接受,于是自己嘗試解讀他的源碼,基于Peak的框架動(dòng)手實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的檢測(cè)工具.
如何判斷內(nèi)存是否泄露
當(dāng)一個(gè)對(duì)象釋放之后,它持有的對(duì)象或者屬性也會(huì)跟著一起釋放(除了單例對(duì)象或者本地持久化的對(duì)象).但是發(fā)生了內(nèi)存泄漏,那么肯定是有某些持有的對(duì)象或者timer是沒(méi)有被銷毀.
那我們只要檢測(cè)這些被持有的對(duì)象在界面消失之后是否還能響應(yīng)事件,即可檢測(cè)出是哪個(gè)地方發(fā)生了內(nèi)存泄漏
.這是上面兩個(gè)框架的基本原理,但是實(shí)現(xiàn)思路卻不同.
MLeaksFinder
MLeaksFinder實(shí)現(xiàn)原理是:在捕獲到這個(gè)
界面銷毀依然存在的對(duì)象
之后,讓它響應(yīng)一個(gè)方法,這個(gè)方法會(huì)觸發(fā)斷言
,斷言會(huì)提示出到底是哪里出現(xiàn)了內(nèi)存泄漏.有興趣的可以去看看上面那篇文章深入了解一下MLeaksFinder的實(shí)現(xiàn)原理.
PLeakSniffer
PLeakSniffer實(shí)現(xiàn)原理是:首先通過(guò)運(yùn)行時(shí)swizzling,獲取需要監(jiān)聽的對(duì)象,例如一個(gè)
ViewController
或者一個(gè)View
,然后給這個(gè)對(duì)象動(dòng)態(tài)添加一個(gè)Detector
屬性,當(dāng)PLeakSniffer
開始監(jiān)聽的時(shí)候,定時(shí)發(fā)送一個(gè)ping
通知,同時(shí)添加個(gè)pong
的觀察者,用于監(jiān)聽Detector
的響應(yīng).Detector
內(nèi)添加了ping
的觀察者,然后為PLeakSniffer
發(fā)送pong
通知,如果當(dāng)前獲取的ViewController
或者一個(gè)View
沒(méi)有被銷毀,那么這個(gè)Detector
會(huì)依然存在,依然會(huì)發(fā)送pong
通知.這時(shí)候就能檢測(cè)到出現(xiàn)內(nèi)存泄漏了.
盜取Peak的原圖,可以更明白其中的原理:
根據(jù)Peak思路自己進(jìn)行實(shí)現(xiàn)一個(gè)檢測(cè)UIViewController內(nèi)存泄漏的工具
首先奉上效果圖:
實(shí)現(xiàn)過(guò)程:
1.新建如下目錄結(jié)構(gòu):
下面逐個(gè)解釋每個(gè)類的作用:
NSObject+Swizzling
#import <Foundation/Foundation.h>
#import "ObjectLeakDetector.h"
@protocol ObjectDelegate<NSObject>
@optional
- (BOOL)isOnScreen;//判斷當(dāng)前控制器是否在屏幕上,用來(lái)判斷是否發(fā)送pong通知
@end
@interface NSObject (Swizzling)<ObjectDelegate>//NSObject遵守這個(gè)協(xié)議
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL;//方法交換
- (void)markObject;//為對(duì)象添加detector對(duì)象,并且發(fā)送ping通知
@property(strong,nonatomic) ObjectLeakDetector *detector;//被檢測(cè)的實(shí)際對(duì)象,是當(dāng)前控制器或者view動(dòng)態(tài)添加持有的對(duì)象
@end
UIViewController+Leaks
#import "UIViewController+Leaks.h"
#import "NSObject+Swizzling.h"
#import <objc/message.h>
@implementation UIViewController (Leaks)
+ (void)load{
[self swizzleSEL:@selector(presentViewController:animated:completion:) withSEL:@selector(t_presentViewController:animated:completion:)];
}
- (void)t_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion{
[self t_presentViewController:viewControllerToPresent animated:flag completion:completion];
#if DEBUG
[viewControllerToPresent markObject];
#endif
}
//如果當(dāng)前controller還在屏幕上
- (BOOL)isOnScreen{
BOOL alive = true;
BOOL visibleOnScreen = false;
UIView* v = self.view;
while (v.superview != nil) {
v = v.superview;
}
if ([v isKindOfClass:[UIWindow class]]) {
visibleOnScreen = true;
}
BOOL beingHeld = false;
if (self.navigationController != nil || self.presentingViewController != nil) {
beingHeld = true;
}
if (visibleOnScreen == false && beingHeld == false) {
alive = false;
}
return alive;
}
@end
這里交換了
UIViewController
的presentViewController:animated:completion:
,然后我們可以拿到viewControllerToPresent
,在這里執(zhí)行[viewControllerToPresent markObject];
,動(dòng)態(tài)給它加上detector
這個(gè)對(duì)象,并且添加ping
通知的觀察者.同時(shí)isOnScreen
判斷了當(dāng)前VC是否還在屏幕上,不在屏幕上的時(shí)候才開始發(fā)送pong
通知.
- (void)setDetector:(ObjectLeakDetector *)detector{
objc_setAssociatedObject(self, @selector(detector), detector, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (ObjectLeakDetector *)detector{
return objc_getAssociatedObject(self, @selector(detector));
}
//給 當(dāng)前類 添加 detector,并且開啟監(jiān)測(cè),這里前面的判斷直接用的peak的代碼
- (void)markObject{
if (self.detector) {
return;
}
//忽略系統(tǒng)類
NSString* className = NSStringFromClass([self class]);
if ([className hasPrefix:@"_"] || [className hasPrefix:@"UI"] || [className hasPrefix:@"NS"]) {
return;
}
//view必須有父類
if ([self isKindOfClass:[UIView class]]) {
UIView* v = (UIView*)self;
if (v.superview == nil) {
return;
}
}
//controller必須有父類
if ([self isKindOfClass:[UIViewController class]]) {
UIViewController* c = (UIViewController*)self;
if (c.navigationController == nil && c.presentingViewController == nil) {
return;
}
}
//生成detector
ObjectLeakDetector *dec = [[ObjectLeakDetector alloc] init];
//給NSObject添加detector
self.detector = dec;
//detector添加ping通知觀察者,并發(fā)送pong通知
[dec sendLeakDetectedNotifacation:self];
}
ObjectLeakDetector
#import "ObjectLeakDetector.h"
#import "NSObject+Swizzling.h"
@interface ObjectLeakDetector()
@property(assign,nonatomic) NSInteger pingCount;
@end
@implementation ObjectLeakDetector
- (void)sendLeakDetectedNotifacation:(NSObject *)detector{
self.weakTarget = detector;
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"ping" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ping:) name:@"ping" object:nil];
}
- (void)ping:(NSNotification *)noti{
//NSLog(@"在發(fā)送pong");
//這里目的只是為了只執(zhí)行一次
if (_pingCount > 3) {
return;
}
if (!self.weakTarget) {
return;
}
//如果當(dāng)前控制器還在屏幕顯示就停止
if (![self.weakTarget isOnScreen]) {
_pingCount ++;
}
//這里不立馬就發(fā)送,延遲一會(huì)兒彈出
if (_pingCount > 3) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"pong" object:self.weakTarget];
}
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"ping" object:nil];
}
@end
這里添加了一個(gè)
pingCount
屬性,是為了讓通知延時(shí)一會(huì)兒發(fā)送出去,這樣可能會(huì)更準(zhǔn)確一點(diǎn).
LeaksManager
@interface LeaksManager : NSObject
+ (instancetype)shareInstance;//單例對(duì)象
- (void)startDetectLeaks;//開始檢測(cè)
@end
在
startDetectLeaks
方法內(nèi),開啟了一定timer用于定時(shí)發(fā)送ping
通知,初始化的時(shí)候添加了pong
的觀察者,用于觀察detector
的響應(yīng).如果發(fā)現(xiàn)內(nèi)存泄漏,就會(huì)通過(guò)alert
的方式進(jìn)行提示.
另外我們?cè)?code>APPDelegate里調(diào)用的時(shí)候,最好是指定為debug
模式下進(jìn)行調(diào)試.
到這里就能實(shí)現(xiàn)一個(gè)很簡(jiǎn)單的UIViewController
的內(nèi)存泄漏檢測(cè)功能了.
Demo在這里.