为什么要写这篇文章
之前一哥们儿去面试,被问到检测内存泄漏的方式,然后他说可以用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内存泄漏的工具
首先奉上效果图:
实现过程:
1.新建如下目录结构:
下面逐个解释每个类的作用:
NSObject+Swizzling
#import
#import "ObjectLeakDetector.h"
@protocol ObjectDelegate
@optional
- (BOOL)isOnScreen;//判断当前控制器是否在屏幕上,用来判断是否发送pong通知
@end
@interface NSObject (Swizzling)//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
@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
这里交换了
UIViewController
的presentViewController: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在这里.