用“僵尸对象”调试内存管理问题

来源:互联网 发布:c语言input是什么意思 编辑:程序博客网 时间:2024/06/01 10:11

Cocoa提供了“僵尸对象”(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。
将NSZombieEnabkled环境变量设为YES,即可开启此功能。比方说,在MAC OS X系统中的bash运行程序时,可以这么做:

export NSZombieEnabled = @"YES"./app

给僵尸对象发消息后,控制台会打印消息,而应用程序则会终止。打印出来的消息就像这样:

*** -[CFString respondsToSelector:]:message sent todeallocated instance 0x7ff9e9c080e0

也可以在Xcode里打开此选项,这样的话,Xcode在运行应用程序时会自动设置环境变量。开启方法为:编辑应用程序的Scheme,在对话框左侧选择“Run”,然后切换至“Diagnostics”分页,最后勾选“Enable Zombie Objects”选项。如图:

这里写图片描述

僵尸的工作原理是什么呢?
答案:它的实现代码深植于Object-C的运行期程序库,Foundation框架及CoreFoundation框架中。系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。

下面代码有助于理解这一步所执行的操作:

#import <Foundation/Foundation.h>#import <objc/runtime.h>@interface EOCClass : NSObject@end@implementation EOCClass@endvoid PrintClassInfo(id obj) {  Class class = object_getClass(obj);  Class superCls = class_getSuperclass(cls);  NSLog(@"=== %s : %s ===",       class_getName(cls),class_getName(superCls));}int main(int argc, char *argv[]) {  EOCClass *obj = [[EOCClass alloc]init];  PrintClassInfo(obj);  [obj release];  NSLog(@"After release:");  PrintClassInfo(obj);

为了便于演示普通对象转化为僵尸对象的过程,这段代码采用了手动引用计数。因为假如使用ARC的话,str对象就会根据代码需要,尽可能多存活一段时间,于是在这个简单的例子中,就不可能变成僵尸对象了,这并是说对象在ARC下绝对不可能转化为僵尸对象。即便用了ARC,也依然会出现这种内存bug,只不过一般要通过稍微复杂些的代码才能表现出来。
范例代码中有个函数,可以根据给定的对象打印出所属的类及其超类的名称。此函数没有直接给对象发送Object-C的class消息,而是调用了运行期库里的object_getClass()函数。因为如果参数已经是僵尸对象了。那么给其发送Object-C消息后,控制台会打印错误消息,而且应用程序会崩溃。范例代码将输出下面这种消息:
Before release:

=== EOCClass: NSObject ===

After release:

=== _NSZombie_EOCClass:nil===

对象所属的类已由EOCClass变为_NSZombie_EOCClass。但是,这个新类是从哪里来的呢?代码中没有定义过这样的一个类。而且,在启用僵尸对象后,如果编译器每看到一种可能变成僵尸的对象,就创建一个与之对应的类,那也太低效了。_NSZombie_EOCClass 实际上是在运行期生成的,当首次碰到EOCClass类的对象要变成僵尸对象时,就会创建这么一个类。创建过程中用到运行期程序库里的函数,它们的功能很强大,可以操作类列表。
僵尸类是从名为NSZombie 的模板类里复制出来的。这些僵尸类没有多少事情可做,只是充当一个标记。接下来介绍它们是怎样充当标记的。首先来看下面这代伪代码,其中演示了系统如何根据需要创建出僵尸类,而僵尸类又如何把待回收的对象转化成僵尸对象。

// Obtain the class of the object being deallocatedClass cls = object_getClass(self);// Get the class's nameconst char *clsName = class_getName(cls);// Prepend _NSZombie_ to the class nameconst char *zombieClsName = @"_NSZombie_" + clsName;// See if the specific zombie class existsClass zombieCls = objc_lookUpClass(zombieClsName);// If the specific zombie class doesn't exists,// then it needs to be createdif(!zombieCls){// Obtain the template  zombie class, where the new class's // name is the prepended string from above   zombieCls = objc_duplicateClass(baseZombieCls,      zombieClsName,0);}// Perform normal destruction of the object being deallocatedobjc_destructInstance(self);// Set the class of the object being deallocated// to the zombie classobjc_setClass(self, zombieCls) // The class of "self" is now _NSZombie_OriginalClass

这个过程其实就是NSObject的delloc方法所做的事。运行期系统如果发现NSZombieEnabled环境变量已设置,那么就把delloc方法“调配”成一个会执行上述代码的版本。执行到程序末尾时,对象所属的类已经变为_NSZombie_OriginalClass了,其中OriginalClass指的是原类名。
代码中的关键之处在于:对象所占内存没有释放,因此,这块内存不可复用。虽说内存泄漏了,但这只是个调试手段,制作正式发行的应用程序时不会把这项功能打开,所以这种泄漏问题无关紧要。
但是,系统为何要给每个变为僵尸的类都创建一个对应的新类呢?这是因为,给僵尸对象发消息后,系统可由此知道该对象原来所属的类。假如把所有僵尸对象都归到NSZombie类里,那原来的类名就丢了。创建新类的工作由运行期函数objc_duplicateClass()来完成,它会把整个NSZombie类结构拷贝一份,并赋予其新的名字。副本类的超类,实例变量及方法都和复制前相同。还有种做法也能保留旧类名,那就是不拷贝NSZombie,而是创建继承自NSZombie的新类,但是用相应的函数完成此功能,其效率不如直接拷贝高。
僵尸类的作用会在消息转发例程中体现出来。NSZombie类并未实现任何方法。此类为超类,因此和NSObject一样,也是个“根类”,该类只有一个实例变量,叫做isa,所有Object-C的根类都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过“完整的消息转发机制”。
在完整的消息转发机制中,_ _ forwarding _ _ 是核心,调试程序时,大家可能在栈回溯消息里看见过这个函数。它首先要做的事情就包括检查接收消息的对象所属的类名。若名称前缀为NSZombie,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息,其中指明了僵尸对象所收到的消息及原来所属的类。然后应用程序就终止了,在僵尸类名中嵌入原始类名的好处,这是就可以看出来了。只要把NSZombie从僵尸类名的开发拿掉,剩下的就是原始类名。下列伪代码演示了这一过程:

// Obtain the  object's  classClass cls = object_getClass(self);// Get the class's nameconst char *clsName = class_getName(cls);// Check if the class is prefixed with _NSZombie_if(string_has_prefix(clsName,"_NSZombie_")) {// If so, this object is a zombie// Get the original class class name by skipping past the// _NSZombie_,i.e. taking the substring from character 10const char *originalClsName = substring_from(clsName, 10);// Get the selector name of the message const char *selectorName = sel_getName(_cmd);// Log a message to indicate which selector is// being sent to which zombie Log("*** -[%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);// Kill the applicationabort();}

把本节开头那个范例补充一下,试着给变成僵尸的EOCClass 对象发送description消息:

EOCClass *obj = [[EOCClass alloc]init];NSLog(@"Bafore release:");PrintClassInfo(obj);[obj release];NSLog(@"After release:");PrintClassInfo(obj);NSString  *desc = [obj description];// 若是开启了僵尸对象功能,那么控制台会输出下列消息:Before release:=== EOCClass: NSObject ===After release:=== _NSZombie_EOCClass:nil===*** -[EOCClass description:]:message sent todeallocated instance 0x7ff9e9c080e0

大家可以看到,这段消息明确指出了僵尸对象所收到的选择子及其原来所属的类,其中还包含接收消息的僵尸对象所对应的“指针值”。

系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnabled可开启此功能。
系统会修改对象的isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为: 打印一条包含消息内容及其接收者的消息,然后终止应用程序。

0 0