您的位置:首页 > 其它

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

2015-08-03 14:39 232 查看
Cocoa提供了“僵尸对象”(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。

将NSZombieEnabkled环境变量设为YES,即可开启此功能。比方说,在MAC OS X系统中的bash运行程序时,可以这么做:

export NSZombieEnabled = @"YES"
./app


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

*** -[CFString respondsToSelector:]:message sent to
deallocated 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
@end

void 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 deallocated
Class cls = object_getClass(self);

// Get the class's name
const char *clsName = class_getName(cls);

// Prepend _NSZombie_ to the class name
const char *zombieClsName = @"_NSZombie_" + clsName;

// See if the specific zombie class exists
Class zombieCls = objc_lookUpClass(zombieClsName);

// If the specific zombie class doesn't exists,
// then it needs to be created

if(!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 deallocated
objc_destructInstance(self);

// Set the class of the object being deallocated
// to the zombie class
objc_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  class
Class cls = object_getClass(self);

// Get the class's name
const 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 10
const 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 application
abort();

}


把本节开头那个范例补充一下,试着给变成僵尸的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 to
deallocated instance 0x7ff9e9c080e0


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

系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnabled可开启此功能。

系统会修改对象的isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为: 打印一条包含消息内容及其接收者的消息,然后终止应用程序。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: