[iOS]自己实现一个简单的内存泄漏检测工具

为什么要写这篇文章

之前一哥们儿去面试,被问到检测内存泄漏的方式,然后他说可以用instruments来调试来检测.面试官问还有其他方式么,他说可以用MLeaksFinder来进行检测,然后面试官继续问知道原理么?后面就不提了.这里讲解了MLeaksFinder原理.这激发了我的好奇心,然后花了些时间去研究这个框架的实现原理,同时令人惊喜的是,在找寻资料的同时发现了MRPeak的这篇文章也是讲解如何实现一个内存泄露检测工具的,而且感觉更容易理解和接受,于是自己尝试解读他的源码,基于Peak的框架动手实现了一个简单的检测工具.

如何判断内存是否泄露

当一个对象释放之后,它持有的对象或者属性也会跟着一起释放(除了单例对象或者本地持久化的对象).但是发生了内存泄漏,那么肯定是有某些持有的对象或者timer是没有被销毁.那我们只要检测这些被持有的对象在界面消失之后是否还能响应事件,即可检测出是哪个地方发生了内存泄漏.这是上面两个框架的基本原理,但是实现思路却不同.

MLeaksFinder

MLeaksFinder实现原理是:在捕获到这个界面销毁依然存在的对象之后,让它响应一个方法,这个方法会触发断言,断言会提示出到底是哪里出现了内存泄漏.有兴趣的可以去看看上面那篇文章深入了解一下MLeaksFinder的实现原理.

PLeakSniffer

PLeakSniffer实现原理是:首先通过运行时swizzling,获取需要监听的对象,例如一个 ViewController或者一个View,然后给这个对象动态添加一个Detector属性,当PLeakSniffer开始监听的时候,定时发送一个ping通知,同时添加个pong的观察者,用于监听Detector的响应.Detector内添加了ping的观察者,然后为PLeakSniffer发送pong通知,如果当前获取的ViewController或者一个View没有被销毁,那么这个Detector会依然存在,依然会发送pong通知.这时候就能检测到出现内存泄漏了.

盗取Peak的原图,可以更明白其中的原理:


根据Peak思路自己进行实现一个检测UIViewController内存泄漏的工具

首先奉上效果图:

Leak.gif

实现过程:
1.新建如下目录结构:
目录结构

下面逐个解释每个类的作用:

NSObject+Swizzling

#import <Foundation/Foundation.h>
#import "ObjectLeakDetector.h"
@protocol ObjectDelegate<NSObject>
@optional
- (BOOL)isOnScreen;//判断当前控制器是否在屏幕上,用来判断是否发送pong通知
@end
@interface NSObject (Swizzling)<ObjectDelegate>//NSObject遵守这个协议
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL;//方法交换
- (void)markObject;//为对象添加detector对象,并且发送ping通知
@property(strong,nonatomic) ObjectLeakDetector *detector;//被检测的实际对象,是当前控制器或者view动态添加持有的对象
@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
}
//如果当前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,在这里执行[viewControllerToPresent markObject];,动态给它加上detector这个对象,并且添加ping通知的观察者.同时isOnScreen判断了当前VC是否还在屏幕上,不在屏幕上的时候才开始发送pong通知.

- (void)setDetector:(ObjectLeakDetector *)detector{
    objc_setAssociatedObject(self, @selector(detector), detector, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (ObjectLeakDetector *)detector{
    return objc_getAssociatedObject(self, @selector(detector));
}
//给 当前类 添加 detector,并且开启监测,这里前面的判断直接用的peak的代码
- (void)markObject{
    
    if (self.detector) {
        return;
    }
    
    //忽略系统类
    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通知观察者,并发送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(@"在发送pong");
    //这里目的只是为了只执行一次
    if (_pingCount > 3) {
        return;
    }
    if (!self.weakTarget) {
        return;
    }
    
    //如果当前控制器还在屏幕显示就停止
    if (![self.weakTarget isOnScreen]) {
        _pingCount ++;
    }
    //这里不立马就发送,延迟一会儿弹出
    if (_pingCount > 3) {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"pong" object:self.weakTarget]; 
    }
}
- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"ping" object:nil];
}
@end

这里添加了一个pingCount属性,是为了让通知延时一会儿发送出去,这样可能会更准确一点.

LeaksManager

@interface LeaksManager : NSObject
+ (instancetype)shareInstance;//单例对象
- (void)startDetectLeaks;//开始检测
@end

startDetectLeaks方法内,开启了一定timer用于定时发送ping通知,初始化的时候添加了pong的观察者,用于观察detector的响应.如果发现内存泄漏,就会通过alert的方式进行提示.
另外我们在APPDelegate里调用的时候,最好是指定为debug模式下进行调试.

到这里就能实现一个很简单的UIViewController的内存泄漏检测功能了.
Demo在这里.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271