首页 > 代码库 > 开发自己的NSZombie
开发自己的NSZombie
前言:如果你不知道什么是zombie技术,请先自行google之。
原文出自:Let‘s Build NSZombie
zombies是一种很有效的调试内存问题的技术。我之前讨论过关于zombies的实现, 今天我们将从头开始由浅入深的自建一个zombie。
回顾
zomibie用于检测内存管理相关的错误。特别用于检测objective-C对象释放后,又向指向该对象的指针发送消息。这种错误常被称为“释放后使用”错误
在通常情况下,这会导致一个消息被发送给了一块已经被重新分配使用或者已经归还给系统内核的内存。如果这块内存已经被系统内核回收,或者已经被重新分配使用,那么就会导致程序崩溃。在内存块被重新分配给一个系的objcetive-C对象使用后,发送的消息可能是跟这个新对象毫无关系的,这样会导致程序跑出一个“方法未实现”的一场,甚至如果该对象有该方法的实现,恰好能正常调用,也会出现一些异常的结果。
也有可能,当对象释放后,他所指向的内存还没来得及回收,还指向源对象,即该对象是一个已经被释放的状态。这样如果还向该对象发消息,会引发一些更加奇怪的问题。例如,如果该对象持有一个UNIX文件描述符,那么有可能会两次close同一个文件描述符,也可能会导致文件描述符被close以后还在被使用,从而导致很多奇怪的bug。
ARC技术已经很好地解决了“释放后使用”的错误,但是也没有彻底清除这类错误。在多线程、非ARC代码、方法申请错误或者ARC类型误用(如OC对象和CF对象的转换)的情况下都有可能会出现该问题。
zombies技术是通过hook对象的dealloc函数实现的。在对象释放的最后不真正释放该对象的内存,zombie改变这些对象的class信息来拦截发给该对象的所有消息。任何发送个“已释放对象(即zombie 对象)”都会检查做检查,而不是像平时那样直接崩溃。
为了编写我们自己的zombies,我们需要hook对象的dealloc函数,然后构造合适的zombie类。步骤大概清楚了,那我们开始吧。
如何捕获所有消息
如果我们创建一个没有任何方法的根类,那么任何发送个该类对象的消息都会进入oc运行时的消息转发机制。这就类似让forwardInvocation:方法转发调用到一个普通的指针。然后,这个转发有些略晚,在forwardInvocation:方法转发消息之前,OC运行时需要一个方法识别符来构造一个NSInvocation对象,也就是methodSignatureForSelector:forwardInvocation:之前调用。如果我们重载这个函数那就能捕获到一个发送给zombie对象的消息。
如何动态创建类
除了捕获消息发送外,zombies也要记住对象的原始类名。然而,对象内没有内存可以保存原始类的引用。如果原始类没有多余的实例变量,那么就没有地方来用于保存了。因此,原始累的信息需要保存在zombie class里而不是zombie对象里,也就是说我们需要动态的创建zombie类。每一个类的对象在dealloc后都会创建与之类对应的zombie类。
下一个问题依然是什么地方保存原始类的信息呢。可以在新创建的类里面也分配一块额外的空间来保存,但是这不是很方便实用。简单的方法就是使用类名。因为objective-c对象命名都是在一个大的命名空间里的,在进程内类名是唯一的。通过在原始类名前面加上特殊的前缀当做zombie类的类名,这样我们既可以通过zombie类名知道原始类名,也可以使用zombie类替换原始类了。我们使用“MAZombie_”作为前缀。
方法实现
注意我们下面的代码都是非ARC的代码,since ARC memory management calls really get in the way here.(不知道怎么翻译好,大家看原文吧)
我们从一个简单的方法实现开始,一个空方法的实现:
<span style="white-space:pre"> </span>void EmptyIMP(id obj, SEL _cmd){}因为OC运行时默认每个类都会实现+initialize方法。这个方法是在类收到第一个调用之前被调用的,允许做一些需要的初始化操作。如果没实现,运行时依然会调用,然后导致消息转发,但是在这里是转发不了的。所以添加一个空实现的+initialize方法可以避免这种问题。EmptyIMP将会作为zombie类的+initialize方法实现。
消息转发使用到的-methodSignatureForSelector:方法的实现会更有趣一些:
NSMethodSignature *ZombieMethodSignatureForSelector(id obj, SEL _cmd, SEL selector) {在该方法内获取对象的类型和类名。这就是zombie类的类名:
Class class = object_getClass(obj); NSString *className = NSStringFromClass(class);原始类的类名可以通过去掉前缀而获得:
className = [className substringFromIndex: [@"MAZombie_" length]];然后输出错误日志,通过调用abort()结束程序让你注意到:
NSLog(@"Selector %@ sent to deallocated instance %p of class %@", NSStringFromSelector(selector), obj, className); abort(); }
创建类
ZombifyClass函数传入一个普通类,然后返回一个zombie class,如果该zombie class不存在就创建一个:
Class ZombifyClass(Class class) {构造zombie类名,并检查zombie类是否存在,不存在则创建它:
NSString *className = NSStringFromClass(class); NSString *zombieClassName = [@"MAZombie_" stringByAppendingString: className];可以通过NSClassFromString判断zombie类是否存在。如果zombie类存在就可以直接返回:
Class zombieClass = NSClassFromString(zombieClassName); if(zombieClass) return zombieClass;注意这里可能会出现相互竞争的情况:如果两个同样类的对象在两个不同的县城同时释放,他们可能同时尝试创建zombie类。在实际代码中你需要在代码段中加锁,以防这种问题的发生。
调用objc_allocateClassPair方法创建zombie类:
zombieClass = objc_allocateClassPair(nil, [zombieClassName UTF8String], 0);然后通过class_addMethod函数添加-methodSignatureForSelector:方法的实现。方法签名"@@::"意味着该方法会返回一个对象,需要传入三个参数,一个对象(self),该方法的方法名(_cmd),以及一个需要获取签名的方法名:
class_addMethod(zombieClass, @selector(methodSignatureForSelector:), (IMP)ZombieMethodSignatureForSelector, "@@::");方法+initialize也需要加上一个空的实现。没有方法需要加入该类的类方法。我们而是需要向该类的类(即元类)加入方法:
class_addMethod(object_getClass(zombieClass), @selector(initialize), (IMP)EmptyIMP, "v@:");现在类已经被创建,可以向运行时注册它,然后返回了:
objc_registerClassPair(zombieClass); return zombieClass; }
zombie化对象
为了让对象变成zombie对象,我们会替换NSObject的dealloc方法的实现。NSObject子类的dealloc方法依然会运行,但是一旦他们调用到NSObject这一层,zombie化的代码将会被执行。它会防止对象不被销毁,同时替换对象的类型为zombie类。这些操作被封装到EnableZombies函数:
void EnableZombies(void) { Method m = class_getInstanceMethod([NSObject class], @selector(dealloc)); method_setImplementation(m, (IMP)ZombieDealloc); }我们可以在main()函数或者类似的敌方调用EnableZombies函数。ZombieDealloc的实现很直接了当。先调用ZombifyClass获取释放对象的zombie类,然后通过object_setClass改变该对象的类型为zombie类:
void ZombieDealloc(id obj, SEL _cmd) { Class c = ZombifyClass(object_getClass(obj)); object_setClass(obj, c); }测试一下,确定能够正常工作:
obj = [[NSIndexSet alloc] init]; [obj release]; [obj count];
随便选了一个NSIndexSet类做测试,为了方便没有选CoreFoundation得类型。开启zombie运行测试代码:
a.out[5796:527741] Selector count sent to deallocated instance 0x100111240 of class NSIndexSet
成功!
总结一下
zombies的实现相当简单。通过动态创建类,我们可以很简单的记录原始类,而不需要任何敌方保存它的信息。methodSignatureForSelector:方法提供了一个方便的点来判断消息是不是发送给zombie对象的。通过hook -[NSObject dealloc]使得对象zombie化以后不会被正真的销毁。
这就是今天的分享。下次再来,你可以发送你对分享主题的一些建议给我,3Q。
第一次翻译,大牛请轻拍,欢迎讨论
以下是我自己写的一个zombie代码:
#import "zombieClass.h" #import <objc/runtime.h> NSLock *lock = [[NSLock alloc] init]; void zombieInitilize(id obj, SEL cmd){ } NSMethodSignature *zombieMethodSignatureForSelector(__unsafe_unretained id obj, SEL cmd, SEL selector){ const char *className = object_getClassName(obj); if (strncmp(className, "zombie_", 7) == 0) { // NSLog(@"error"); exit(0); } return nil; } Class createZombieClass(Class cla){ const char*className = object_getClassName(cla); char zombieClassName[1024] = {0}; sprintf(zombieClassName, "zombie_%s", className); Class zombieClass = objc_getClass(zombieClassName); if (zombieClass) { return zombieClass; } [lock lock]; zombieClass = objc_allocateClassPair(nil, zombieClassName, 0); if (!zombieClass) { [lock unlock]; return objc_getClass(zombieClassName); } class_addMethod(zombieClass, @selector(methodSignatureForSelector:), (IMP)zombieMethodSignatureForSelector, "@@::"); class_addMethod(objc_getClass("zombieClass"), @selector(initialize), (IMP)zombieInitilize, "v@:"); objc_registerClassPair(zombieClass); [lock unlock]; return zombieClass; } void zombieDealloc(__unsafe_unretained id _self, SEL func){ objc_removeAssociatedObjects(_self); Class cla = createZombieClass(object_getClass(_self)); object_setClass(_self, cla); return ; } void enableZombie(){ Method method = class_getClassMethod([NSObject class], NSSelectorFromString(@"dealloc")); const char *type = method_getTypeEncoding(method); class_replaceMethod([NSObject class], NSSelectorFromString(@"dealloc"), (IMP)zombieDealloc, type); }
开发自己的NSZombie