新款Objective-C内存泄漏自动检测工具PLeakSniffer,GitHub地址。
背景
前些天读到WeRead团队分享的一款内存泄漏检测工具MLeaksFinder,恍惚想起早些时候自己也有过编写这样一个小工具的想法,不知道由于什么原因把这事给忘记了。在仔细读过MLeaksFinder源码,了解实现思路之后,发现和自己最初的想法并不相同,终于在上个周末战胜拖延症将之前的想法付诸于代码,也就诞生了这款功能类似的内存泄漏检测工具PLeakSniffer。建议读者先详细阅读下MLeaksFinder这篇博客。
为什么要再造轮子
我在公司的项目里实际试用了MLeaksFinder,还查处了2处泄漏??。根据MLeaksFinder代码文件中日期推测,这个项目至少已开始半年有余,并在微信读书上得到了实践验证,在功能性和稳定性上都应该有不错的表现。
在编写完PLeakSniffer之后,查出了与MLeaksFinder相同的内存泄漏,思路迥异的代码抵达了相同的终点,写代码的乐趣莫过于此。新的思路或许还能抛砖引玉,如果激发更多的创意,也算是对iOS开发社区的一点小贡献。
MLeaksFinder现阶段能查处UIViewController和UIView的泄漏,我早先的想法还能递归的查出UIViewController之下所有Property的泄漏,并在PLeakSniffer及公司项目中得到了初步的验证,这算是对MLeaksFinder功能的一个小补充。
这类工具的意义
在我们讨论这类工具的意义之前,我们先得明确一点:
如果不使用Instrument当中的Leak检测工具,并没有什么轻易的100%精准的内存泄漏检测方式。
但这类工具还是有其存在价值的,内存泄漏的危害不用赘述,如果有一款工具能在80%的场景下检测出可能的内存泄漏,而且这种检测并不会带来任何副作用(不影响生产环境代码),为什么不使用它呢。
大部分人都低估了他们写代码时导致意外内存泄漏的可能性。Retain Cycle,Block强引用,NSTimer释放不当,这些常见的错误还是很容易出现在我们的代码里,Instrument每使用一次要费些精力,适合做定期的大排查。平常时候就更适合用MLeaksFinder,PLeakSniffer这类工具来做实时监控,提供免费建议。
PLeakSniffer实现思路
我们绝大部分时候都是在编写UIViewController,UIViewController就像一个根节点,持有并管理着很多的子节点对象,这些子节点的生命周期都依赖于Controller,Controller释放的时候,他们也随之释放。用一张图简单的描述他们的关系:
根据各个应用使用的设计模式不同(MVC,MVP,MVVM等),Controller所持有的Property也不相同。这里我们使用MVP作为例子,Controller所包含的对象就包括各种View对象,和Presenter,Model对象。当然每个对象又有可能持有更多的子对象。
PLeakSniffer基于这样一个假设:
如果Controller被释放了,但其曾经持有过的子对象如果还存在,那么这些子对象就是泄漏的可疑目标。
当然这个假设并不是一个100%适用的真理,不同工程师编写代码的方式风格差别很大,有些会把某些UIViewController做成单例(个人觉得这不是个好主意。。),有些会把某些View缓存起来(即使Controller已被释放),还会有其他考虑不到的场景。但在80%以上的场景,我们在Controller结束生命周期之后会将其持有的资源一并释放。这时候PLeakSniffer可以发挥用处,给你一些免费的泄漏建议。
那么怎么在Controller被释放之后,知道其持有的对象没有被释放呢?
一个小技巧可以达成这个目标:子对象(比如view)建立一个对controller的weak引用,如果Controller被释放,这个weak引用也随之置为nil。那怎么知道子对象没有被释放呢?用一个单例对象每个一小段时间发出一个ping通知去ping这个子对象,如果子对象还活着就会一个pong通知。所以结论就是:如果子对象的controller已不存在,但还能响应这个ping通知,那么这个对象就是可疑的泄漏对象。完整的结构可以用下图表示:
通知移除需要一个时机,这里我们使用Associated Object机制给每一个子对象再生成一个Proxy对象,在Proxy对象的dealloc里面移除通知。
当然什么时候去判断一个对象的生命周期开始,什么时候判断为结束,需要一个精挑细选的机制。View,Controller,Property各不相同。
PLeakSniffer采取保守的策略,通过Objective C的runtime机制,递归的将一个Controller所有强引用的property找出,并安装proxy监听Ping通知。在我的测试下,基本上能将property泄漏的场景找出。
PLeakSniffer的使用方式很简答,通过Pod安装后,通过以下代码激活即可。
#if MY_DEBUG_ENV
[[PLeakSniffer sharedInstance] installLeakSniffer];
[[PLeakSniffer sharedInstance] addIgnoreList:@[@"MySingletonController"]];
#endif
addIgnoreList可以添加一些特殊的忽略名单,比如单例这种无法正确预测泄漏的对象。切记用Debug的宏将上述代码包住,不要把这些检测泄漏的代码带进线上环境。
如果检测到可疑泄漏,PLeakSniffer会在控制台打印一条日志:
Controller泄漏:Detect Possible Controller Leak: %@
其他对象泄漏:Detect Possible Leak: %@
更多的细节请查阅代码:GitHub地址。
import "UIViewController+PLeak.h"
怎么判断UIViewController没有释放
- (BOOL)isAlive
{
BOOL alive = true;
BOOL visibleOnScreen = false;
UIView* v = self.view;
while (v.superview != nil) {
v = v.superview;
}
if ([v isKindOfClass:[UIWindow class]]) {
visibleOnScreen = true;
}
/// 当UIViewController的顶级superview,不是UIWindow,说明控制器被pop或者diss了
/// 当控制器被pop或者disss时self.navigationController, self.presentingViewController为空,
BOOL beingHeld = false;
if (self.navigationController != nil || self.presentingViewController != nil) {
beingHeld = true;
}
//not visible, not in view stack
/// 当alive == false是,说明控制器还存在,没被释放
if (visibleOnScreen == false && beingHeld == false) {
alive = false;
}
if (alive == false) {
// PLeakLog(@"leaked object: %@ ?", [self class]);
}
return alive;
}
获取属性方法
//
// NSObject+PLeakTrack.m
// PIXY
//
// Created by gao feng on 16/7/2.
// Copyright © 2016年 instanza. All rights reserved.
//
#import "NSObject+PLeakTrack.h"
#import "NSObject+PLeak.h"
#import <objc/runtime.h>
@implementation NSObject (PLeakTrack)
- (void)watchAllRetainedProperties:(int)level
{
if (level >= 5) { //don't go too deep
return;
}
NSMutableArray* watchedProperties = @[].mutableCopy;
//track class level1
NSString* className = NSStringFromClass([self class]);
if ([className hasPrefix:@"UI"] || [className hasPrefix:@"NS"] || [className hasPrefix:@"_"]) {
return;
}
NSArray* l1Properties = [self getAllPropertyNames:[self class]];
[watchedProperties addObjectsFromArray:l1Properties];
//track class level2
NSString* superClassName = NSStringFromClass([self superclass]);
if ([superClassName hasPrefix:@"UI"] == false &&
[superClassName hasPrefix:@"NS"] == false &&
[superClassName hasPrefix:@"_"] == false)
{
NSArray* l2Properties = [self getAllPropertyNames:[self superclass]];
[watchedProperties addObjectsFromArray:l2Properties];
}
//track class level3
if ([[self superclass] superclass]) {
NSString* superSuperClassName = NSStringFromClass([[self superclass] superclass]);
if ([superSuperClassName hasPrefix:@"UI"] == false &&
[superSuperClassName hasPrefix:@"NS"] == false &&
[superSuperClassName hasPrefix:@"_"] == false)
{
NSArray* l3Properties = [self getAllPropertyNames:[[self superclass] superclass]];
[watchedProperties addObjectsFromArray:l3Properties];
}
}
for (NSString* name in watchedProperties) {
id cur = [self valueForKey:name];
if (cur) {
BOOL ret = [cur markAlive];
if (ret) {
[cur pProxy].weakHost = self;
[cur watchAllRetainedProperties:level+1];
}
}
// SEL setter = [self setterForPropertyName:name];
// if (setter) {
// __weak __typeof(self) wself = self;
//
// //check current value
// id cur = [self valueForKey:name];
//
// if (cur) {
//
// BOOL ret = [cur markAlive];
// if (ret) {
// [cur pProxy].weakHost = wself;
// [cur watchAllRetainedProperties:level+1];
// }
// }
//
// //check future value
// [self aspect_hookSelector:setter withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, id newValue){
//
// if (newValue != nil) {
// BOOL ret = [newValue markAlive];
// if (ret) {
// [newValue pProxy].weakHost = wself;
// [newValue watchAllRetainedProperties:0];
// }
// }
//
// } error:nil];
//
// }
}
}
- (void)didObserveNewValue:(id)value
{
if (value) {
BOOL ret = [value markAlive];
if (ret) {
[value pProxy].weakHost = self;
[value watchAllRetainedProperties:0];
}
}
}
- (BOOL)isAlive
{
BOOL alive = true;
if (self.pProxy.weakHost == nil) {
alive = false;
}
return alive;
}
#pragma mark- mess with runtime
- (NSArray*)getAllPropertyNames:(Class)cls
{
unsigned int i, count = 0;
objc_property_t* properties = class_copyPropertyList(cls, &count );
if(count == 0)
{
free(properties);
return nil;
}
NSMutableArray* names = @[].mutableCopy;
for (i = 0; i < count; i++)
{
objc_property_t property = properties[i];
NSString* typeName = @"";
const char* str = property_getTypeString(property);
if (str != NULL) {
typeName = [NSString stringWithUTF8String:str];
}
NSString* name = [NSString stringWithUTF8String:property_getName(property)];
//we are only interested in a very limited group of types
if ([typeName isEqualToString:@"T@\"PObjectProxy\""] ||
[typeName hasPrefix:@"T@\"UI"] ||
[typeName hasPrefix:@"T@\"NS"] ||
[typeName rangeOfString:@"KVO"].location != NSNotFound) {
continue;
}
bool isStrong = isStrongProperty(property);
if (isStrong == false)
{
continue;
}
[names addObject:name];
}
return names;
}
- (SEL)setterForPropertyName:(NSString*)name
{
objc_property_t property = class_getProperty([self class], [name UTF8String]);
if (property == nil)
return nil;
SEL result = property_getSetter(property);
if (result != nil)
return result;
NSMutableString* setterName = @"set".mutableCopy;
NSString* upcaseName = [name stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[name substringToIndex:1] capitalizedString]];
[setterName appendString:upcaseName];
[setterName appendString:@":"];
if ([[self class] instancesRespondToSelector:NSSelectorFromString(setterName)] == false)
{
return nil; //no proper setter found
}
return NSSelectorFromString(setterName);
}
SEL property_getSetter(objc_property_t property)
{
const char* attrs = property_getAttributes(property);
if (attrs == nil)
return nil;
const char* p = strstr(attrs, ",S");
if (p == nil)
return nil;
p += 2;
const char* e = strchr(p, ',');
if (e == nil)
{
return sel_getUid(p);
}
if (e == p)
{
return nil;
}
int len = (int)(e - p);
char* selPtr = malloc(len + 1);
memcpy(selPtr, p, len);
selPtr[len] = '\0';
SEL result = sel_getUid(selPtr);
free(selPtr);
return result;
}
bool isStrongProperty(objc_property_t property)
{
const char* attrs = property_getAttributes( property );
if (attrs == NULL)
return false;
const char* p = attrs;
p = strchr(p, '&');
if (p == NULL) {
return false;
}
else
{
return true;
}
}
const char* property_getTypeString( objc_property_t property )
{
const char * attrs = property_getAttributes( property );
if (attrs == NULL)
return NULL;
static char buffer[256];
const char * e = strchr( attrs, ',' );
if (e == NULL)
return NULL;
int len = (int)(e - attrs);
memcpy(buffer, attrs, len);
buffer[len] = '\0';
return buffer;
}
@end