一起来构建NSZombie


僵尸对象(NSZombie)用于调试内存管理问题的时候是相当有用的,此前我有谈论过僵尸对象(NSZombie)的实现,在ШпиркоАлексей 的建议下,今天我们要谈一谈如何从头开始构建这个类。


回顾


僵尸对象(NSZombie)能用于嗅探内存错误,更确切的说法是,当某一指针发送消息到已经释放的Objective-C对象的时候,僵尸对象就能检测到。一个比较常见的就是"use after free"错误。


在通常情况下,这样的错误会导致消息发送到已被重写的内存,或者这段内存已经返回给内核了。无论是哪种结果,都会引发冲突。如果说这段内存恰好被一个新的Objective-C对象重写,那么消息可能就会发送给与原来风马牛不相及的对象,而且会由于无法识别方法选择器而抛出异常,假如这个错误的对象也有该方法的话,那么就会发生更奇怪的问题。


当然也有可能这段内存中存放的对象尚未被更改,处于将被销毁的状态,那么这会引起更匪夷所思的错误。举个例子,假如该对象里面包含有一个UNIX文件句柄,那么就有可能重复调用两次关闭文件,而其中重复关闭的文件描述符也许是程序里其他地方打开的文件,这样的情况导致的错误会更为奇葩,你发现错误的时候甚至找不到bug在哪。


虽然ARC(Automatic Reference Counting)机制大大的减少了这类错误的发生,然而并非能斩草除根。由于各种各样的原因问题还是要发生,比如多线程应用、没有开启ARC机制、错配方法声明或者类型系统本身存在改写ARC存储编辑器的弊端。


僵尸对象会与对象的回收挂钩,这时候对象回收的最后一步并不是真正的释放所占内存,而是将其改为一个僵尸类,并截断所有发送给该对象的消息。这样,在有消息发给该对象时候都会检测到出错信息,而不是像正常调试下那样出现五花八门的状况。当然也有在重写对象之后再释放内存的方式,不过这样做的话就达不到什么目的了,因为释放的内存很快又会被占用,我一般都会忽略掉这种做法。


要实现这种机制,就需要先实现与对象回收挂钩,然后再构建适当的僵尸类型,那么下面就开始吧。


捕获所有消息


假如构建一个空的类,其中没有任何方法,那么任何发到这个类实例的消息都会进入消息转发机制。显然这个时候会调用forwardInvocation:方法来完成消息重定向,不过这一方法的调用其实要晚一些,在它运行前,运行时需要一个方法来创建NSInvocation对象,也就是说,methodSignatureForSelector:会先执行。吶,这个方法就是我们需要重写来为僵尸类发送消息的重点。


动态分配


除了发送的选择器之外,僵尸对象还需记录覆盖对象的原本类型。然而,对于僵尸对象来说,不一定有足够的空间来存储一个对象原本类型的应用。如果对于该类没有其他的实例变量,而僵尸对象又不能重新为其分配存储空间,那么在僵尸类定义的时候就应该预留空间来存储覆盖空间原本的类型。这也意味着僵尸对象的内存空间必须是动态分配的,对于每个类来说,一旦它有某个实例僵死(被回收),它就会拥有自己的僵尸类。


那么问题来了,这段内存原本对应的类的引用应该存放在哪。使用额外的存储空间来分配这个类的存储是可行的,但是在使用起来的时候并不方便。一个较为简单的方法就是使用类名,因为Objective-C中每个类都存在于一个大的命名空间中,所以对于辨识一个类来说只要用类名就够了。给该类名加上前缀并用来命名僵尸类,这样就既可以描述该类本身,又能从其类名还原本来的类。在这里我用的是MAZombie_作为前缀。


方法的实现


请注意本文中所有的代码均默认没有开启ARC功能,因为使用ARC内存管理的话会影响NSZombie的构建,有碍观瞻。


我们从一个最简单的方法开始实现,下面是一个空方法:


1
void  EmptyIMP(id obj, SEL _cmd) {}


在Objective-C中每个类及其子类在被第一次发送消息前都会先调用它的+initialize方法,从而使其能够初始化本身。如果运行时发现该类没有实现+initialize方法,就会将消息转发,当然在此如果让消息转发的话我们的僵尸类也就没什么用了。所以需要添加一个空的+initialize方法来避免这个问题,EmptyIMP方法则就是作为僵尸类中+initialize方法的实现而构造的。

而-methodSignatureForSelector:方法则更有意思:


1
NSMethodSignature *ZombieMethodSignatureForSelector(id obj, SEL _cmd, SEL selector) {


原内存中的对象的类型可以从中提取,在僵尸类里面是这样的:


1
2
Class  class  = object_getClass(obj);
NSString *className = NSStringFromClass( class );


去掉类名的前缀就可以提取原本类型的名称了:


1
className = [className substringFromIndex: [@ "MAZombie_"  length]];


接着记录出错信息并调用abort()来确保你能够注意到:


1
2
3
NSLog(@ "Selector %@ sent to deallocated instance %p of class %@" , NSStringFromSelector(selector), obj, className);
         abort ();
     }


创建类


ZombifyClass方法用于从一个普通类生成一个僵尸类,如果必要的话可以像这样创建它:


1
Class ZombifyClass(Class  class ) {


僵尸类的类名在用于检测其是否存在时相当有用,当然如果不存在的情况下也要用到这个类名:


1
2
NSString *className = NSStringFromClass( class );
NSString *zombieClassName = [@ "MAZombie_"  stringByAppendingString: className];


使用NSClassFromeString来检测僵尸类是否存在,然后如果存在的话会返回一个僵尸类:


1
2
Class zombieClass = NSClassFromString(zombieClassName);
if (zombieClass)  return  zombieClass;


请注意这里会产生一个竞争机制:如果有同一个类的两个实例分别在不同的线程中同时僵死的话,它们都会试图创建僵尸类。在即使实际编码的时候你最好对这一段代码进行上锁,以保证类似的冲突不会发生:


调用objc_allocateClassPair方法为僵尸类分配内存:


1
zombieClass = objc_allocateClassPair(nil, [zombieClassName UTF8String], 0);


使用class_addMethod来添加methodSignature方法的实现。其中的参数"@@::"表示其返回的带有三个参数的对象:一个对象(self),一个选择器(_cmd),以及另一个选择器(确切的选择器参数):


1
class_addMethod(zombieClass, @selector(methodSignatureForSelector:), (IMP)ZombieMethodSignatureForSelector,  "@@::" );


前面的空方法一样也要添加到+initialize方法的实现中,并没有单独的函数用于添加类方法,所以我们要将方法添加到类的类中,也就是元类中(Objective-C的类其实也可以看作是对象,即类对象,这个类对象的类就是元类):


1
class_addMethod(object_getClass(zombieClass), @selector(initialize), (IMP)EmptyIMP,  "v@:" );


现在僵尸类已经全部设置完毕,接着它就可以在运行时注册并返回了:


1
2
3
objc_registerClassPair(zombieClass);
         return  zombieClass;
     }


对象僵尸化


为了能够使得对象能够僵尸化,我们需要重写NSObject的dealloc方法。子类的dealloc方法照旧,不过一旦dealloc回溯到NSObject的时候,有关僵尸化的代码就会运行了。这段代码会防止对象被彻底的销毁,然后确保用一个僵尸类来保存这个对象的信息。这些操作都由一个函数来封装实现:


1
2
3
4
void  EnableZombies( void ) {
         Method m = class_getInstanceMethod([NSObject  class ], @selector(dealloc));
         method_setImplementation(m, (IMP)ZombieDealloc);
     }


我们可以在main()函数或者其他相应的地方调用EnableZombies(),剩下的事就由它自行解决了。下面这个ZombieDealloc函数简单明了,它会调用ZombifyClass来给需要回收的对象生成僵尸类型,然后用object_setClass来将此对象的类型更改为僵尸类:


1
2
3
4
void  ZombieDealloc(id obj, SEL _cmd) {
         Class c = ZombifyClass(object_getClass(obj));
         object_setClass(obj, c);
     }


测试


现在需要确保下面这段能正常运行:


1
2
3
obj = [[NSIndexSet alloc] init];
     [obj release];
     [obj count];


我在这里算是很随意的用了NSIndexSet类,该类无需与CoreFoundation框架作奇怪的桥接。应用僵尸类运行之后的结果如下:


1
a.out[5796:527741] Selector count sent to deallocated instance 0x100111240 of  class  NSIndexSet


大功告成~


总结


僵尸类的实现按说相当简单。通过动态的给类分配空间,我们可以在无需依赖僵尸对象内部空间的情况下保存对象原本类型的信息。methodSignatureForSelector:方法为截断发送给僵尸类的消息提供了便于突破的节点。仅需要挂钩-[NSObject dealloc]方法就可以将一般对象转换为僵尸对象,而不用在其减少为零的时候销毁。


来源:CocoaChina

原文地址:http://www.cocoachina.com/ios/20141204/10526.html


猜你喜欢

转载自blog.csdn.net/wangshengfeng1986211/article/details/42914031