Notes and thoughts from Tony

使用僵尸对象辅助调试

Comments

前言

以前听过僵尸对象,原来只是觉得大概是内存没有被引用,但是依然没有被覆写的状态。

NSZombie Object

Cocoa提供了“僵尸对象(NSZombie Object)”功能。启用这个功能之后,运行期系统会把所有已经回收的实例转化成为特殊的“僵尸对象”,而不会真正回收它们(大概就是死而不僵,相当形象了)。这种对象所在的核心内存无法重用,因此不可能早到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。

当我们对已回收的对象发送消息时,崩溃不是每次都会发生的:具体是否会崩溃取决于对象所占内存有没有被其他内容所覆写。

使用方法

Xcode中将NSZombieEnabled设置为YES,即可开启此项功能。具体为,打开Edit Scheme,选择run,切换至Diagnostics分页,最后勾选Enable Zombie Objects选项。

01

原理

NSZombie Object的实现代码在OC的runtime库、Foundation框架及CoreFoundation框架中(暂时没有勇气去找虐)。系统在即将回收对象时,如果发现启用僵尸对象功能,则把对象转化为僵尸对象,而不彻底回收。

由于在ARC下需要比较复杂的代码才能表现出僵尸对象。所以写几句MRC的代码来简单表示下:

void PrintClassInfo(od obj) {
    Class cls = object_getClass(obj);
    Class superCls = class_getSuperclass(cls);
    NSLog(@"---%s : %s---", class_getName(cls), class_getName(superCls));
}

int main(int argc, char *argv[]) {
    Person *p = [[Person alloc] init];
    NSLog(@"Before release:");
    PrintClassInfo(p);
    [p release];
    NSLog(@"After release:");
    PrintClassInfo(p);
}

PrintClassInfo的作用是用来打印对象所属类及父类。输出为:

Before release:
---Person : NSObject ---
After release:
---_NSZombie_Person : nil ---

对象所述的类已经由Person变为_NSZombie_Person_NSZombie_Person实际是在运行期生成的,当首次遇到Person的对象要变成僵尸对象时,就会创建这么一个类。创建过程中用到了runtime,然后操作了类列表(class list)。

僵尸类(zombie class)是从名为_NSZombie_的莫办理复制出来的。这些僵尸类没有多少事情可做,只是充当一个标记。下面的伪代码演示了系统如果根据需要创建僵尸类,而僵尸类又如何把待回收的对象转化为僵尸对象。

//获取要被dealloc的对象的类
Class cls = object_getClass(self);

//获取类名
const char *clsName = class_getName(cls);

//获取僵尸类名
const char *zombieClsName = "_NSZombie_" + clsName;

//查看是否类已存在
Class zombieCls = objc_lookUpClass(zombieClsName);

//如果不存在,则需要被创建
if(!zombieCls){
    //获取_NSZombie_类
    Class baseZombieCls = objc_lookUpClass("_NSZombie_");
    
    //复制_NSZombie_类,并以新类名命名
    zombieCls = objc_duplecateClass(baseZombieCls, zombieClsName, 0);
}

//手动释放对象
objc_destructInstance(self);

//设置已经被dealloc的类为僵尸类,至此,这个对象已经变成了僵尸对象
objc_setClass(self, zombieCls);

这个方式实际是NSObjectdealloc方法所做的事儿。运行时系统如果发现NSZombieEnabled=YES,则将dealloc交换为上面的代码。执行到最后时,对象所属的类已经变成_NSZombie_Person了。

代码的关键在于:对象所占的内存没有(通过调用free()函数)释放,因此,这个内存不可复用。当然这只是调试手段,正式发布的程序不会这么作死。

之所以为每个变为僵尸的类都对应创建一个新类,是因为向僵尸对象发送消息后,系统可知道该对象原来所属的类。否则所有僵尸对象对属于_NSZombie_类,就没法调试了。创建新类的工作由运行时函数objc_destructInstance()完成,它会把整个_NSZombie_类结构拷贝一份,并赋予新的名字。副本的父类、实例变量和方法都和复制之前相同。还有种方法也能保留旧类名,就是继承自_NSZombie_类,但是效率没有直接拷贝高。

僵尸类的作用会在消息转发的过程中体现出来。_NSZombie_类没有实现任何方法,也没有父类。只有一个实例变量isa(所有的oc根类比如NSObject都必须有此变量)。由于这个类没有实现任何方法,所有发给它的消息都会经过full forwarding mechanism

在完整的消息转发机制中,___forwarding___是核心,调试程序时,可以在调用栈中看到这个函数。它首先做的就是包括检查接受消息的对象所属的类名。若名称前缀为_NSZombie_则表明消息接受者是僵尸对象,需要特殊处理。此时会打印一条消息:

*** -[receiver messageSelector:]: sent to deallocated instance 0x7ff9e9c080e0

其中指明了僵尸对象所收到的消息及原来所属的类,然后应用程序会终止。可以从僵尸类名中获取原类的名字。

//获取对象类
Class cls = object_getClass(self);

//获取类名
const char *clsName = class_getName(cls);

//检查类名是不是以_NSZombie_开头
if (string_has_prefix(clsName, "_NSZombie_")) {
    //是个僵尸对象
    //获取原来的类名
    const char *originalClsName = substring_from(clsName, 10);
    
    //获取方法名
    const char *selectorName = sel_getName(_cmd);
    
    //打印信息
    Log("*** -[%s %s]: message sent to deallocated instance %p", originalClsName, selectorName,self);
    
    //终止程序
    about();
}

本文开头的示例代码若开启了僵尸对象功能,并给release后Person的实例发送description消息,输出为:

*** -[Person description]: message sent to deallocated instance 0x7f8123c1ac200

总结

  • 调试时可以通过开启僵尸对象功能来将原来要被回收的对象转化为僵尸对象。

  • 系统会修改对象的isa指针,将其指向僵尸类。僵尸类能相应所有的消息,打印一条包含消息内容及接受者的消息,然后终止应用程序。

参考:

Effective Objective-C 2.0