[iOS]自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的內(nèi)存泄漏檢測(cè)工具

為什么要寫這篇文章

之前一哥們兒去面試,被問(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)存泄漏的工具

首先奉上效果圖:

Leak.gif

實(shí)現(xiàn)過(guò)程:
1.新建如下目錄結(jié)構(gòu):
目錄結(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

這里交換了UIViewControllerpresentViewController: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在這里.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末就珠,一起剝皮案震驚了整個(gè)濱河市国瓮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌雇锡,老刑警劉巖搁痛,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異逼侦,居然都是意外死亡匿辩,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門榛丢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)铲球,“玉大人,你說(shuō)我怎么就攤上這事晰赞〖诓。” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵掖鱼,是天一觀的道長(zhǎng)然走。 經(jīng)常有香客問(wèn)我,道長(zhǎng)锨用,這世上最難降的妖魔是什么丰刊? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮增拥,結(jié)果婚禮上啄巧,老公的妹妹穿的比我還像新娘。我一直安慰自己掌栅,他們只是感情好秩仆,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著猾封,像睡著了一般澄耍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晌缘,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天齐莲,我揣著相機(jī)與錄音,去河邊找鬼磷箕。 笑死选酗,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的岳枷。 我是一名探鬼主播芒填,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼呜叫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了殿衰?” 一聲冷哼從身側(cè)響起朱庆,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闷祥,沒(méi)想到半個(gè)月后娱颊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜀踏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年维蒙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掰吕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片果覆。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖殖熟,靈堂內(nèi)的尸體忽然破棺而出局待,到底是詐尸還是另有隱情,我是刑警寧澤菱属,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布钳榨,位于F島的核電站,受9級(jí)特大地震影響纽门,放射性物質(zhì)發(fā)生泄漏薛耻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一赏陵、第九天 我趴在偏房一處隱蔽的房頂上張望饼齿。 院中可真熱鬧,春花似錦蝙搔、人聲如沸缕溉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)证鸥。三九已至,卻和暖如春勤晚,著一層夾襖步出監(jiān)牢的瞬間枉层,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工赐写, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸟蜡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓血淌,卻偏偏與公主長(zhǎng)得像矩欠,于是被迫代替她去往敵國(guó)和親财剖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345