第35条:用“僵尸对象”调试内存管理问题

  本条要点:(作者总结)  

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

  调试内存管理问题很令人头疼。大家都知道,向已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移作他用。又无法确定,因此,应用程序只是偶尔崩溃。在没有崩溃的情况下,那块内存可能只复用了其中一部分,所以对象中的某些二进制数据依然有效。还有一种可能,就是那块内存恰好为另外一个有效且存活的对象所占据。在这种情况下,运行期系统会把消息发到新对象那里,而此对象也许能应答也许不能。如果能,那程序就不崩溃,可你会觉得奇怪:为什么收到消息的对象不是预想的那个呢?若新对象无法响应选择子,则程序依然会崩溃。

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

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

  export NSZomebieEnabled = "YES";

  ./app

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

  *** -[CFString respondsToSelector:]:message sent to

  deallocated instance 0x7ff9e9c080e0

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

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

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

 1   #import <Foundation/Foundation.h>
 2 
 3   #import <objc/runtime.h>
 4 
 5   @interface EOCClass : NSObject
 6 
 7   @end
 8 
 9   
10 
11   @implementation EOCClass
12 
13   @end
14 
15   void PrintClassInfo(id obj) {
16 
17     Class cls = object_getClass(obj);
18     Class superCls = class_getSuperclass(cls);
19     NSLog(@"=== %s: %s ===", class_getName(cls), class_getName(superCls));
20   }
21 
22   
23 
24   int main(int argc, char *argv[]) {
25 
26     EOCClass *obj = [[EOCClas alloc] init];
27     NSLog(@"Before release");
28     PrintClassInfo(obj);
29     [obj release];
30     NSLog(@"After release");
31     PrintClassInfo(obj);
32   }

  为了便于演示普通对象转化为僵尸对象的过程,这段代码采用了手动引用计数。因为假如使用 ARC 的话,str 对象就会根据代码需要,尽可能多存活一段时间,于是在这个简单的例子中,就不可能变成僵尸对象了。这并不是说对象在 ARC 下绝对不可能转化为僵尸对象。即便用了 ARC ,也依然会出现这种内存 bug,只不过一般要通过稍微复杂些的代码才能表现出来。

  范例代码中有个函数,可以根据给定的对象打印出所属的类及其超类名称。此函数没有直接给对象发送 Objective-C 的 class 消息,而是调用了运行期库里的 object_getClass() 函数。因为如果参数已经是僵尸对象了,那么给其发送 Objective-C 消息后,控制台会打印错误消息,而且应用程序会崩溃。范例代码将输出下面这种消息:

1   Before release
2 
3   === EOCClass : NSObject ===
4 
5   After release
6 
7   === NSZombie_EOCClass : nil ===

  对象所属的类已由 EOCClass 变为 _NSZombie_EOCClas。但是,这个新类是从哪里来的呢?代码中没有定义一个这样的类。而且,在启用僵尸对象后,如果编译器每看到一种可能变成僵尸的对象。就创建一个与之对应的类,那也太低效了。_NSZombie_EOCClass 实际上是在运行期生成的,当首次碰到 EOCClass 类的对象要变成僵尸对象时,就会创建这么一个类。创建过程中用到了运行期程序库里的函数,它们的功能很强大,可以操作类列表(class list)。

  僵尸类(zombie class)是从名为 _NSZombie_ 的模板类里复制出来的。这些僵尸类没有多少事情可做,只是充当一个标记。接下来介绍它们是怎样充当标记的。首先来看下面这段伪代码,其中演示了系统如何根据需要创建出僵尸类,而僵尸类又如何把待回收的对象转化成僵尸对象。

 1 // Obtain the class of the object being deallocted
 2 
 3 Class cls = object_getClass(self);
 4 
 5  
 6 
 7 // Get the class's name
 8 
 9 const char *clsName = class_getName(cls);
10 
11 //Prepend _NSZombie_ to the class name
12 
13 const char *zombieClsName = "_NSZombie_" + clsName;
14 
15  
16 
17 // See if the specifie class exists
18 
19 Class zombieCls = objc_lookUpClass(zombieClsName);
20 
21  
22 
23 // If the specific zombie class doesn't exist, then it needs to be created
24 
25 if (!zombieCls) {
26 
27   // Obtain the template zombie class called _NSZombie_
28 
29   Class baseZombieCls = objc_lookUpClass("_NSZombie_");
30 
31  
32 
33   // Duplicate the base zombie class, where the new class's 
34 
35   // name is the prepended string from above
36 
37   zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
38 
39 }
40 
41 // Perform normal destruction of the object being deallocated
42 
43 objc_destructInstance(self);
44 
45 // Set the class of the object being deallocated
46 
47 // to the zombie class
48 
49 objc_setClass(self, zombieCls);
50 
51  
52 
53 // The class of 'self' is now _NSZombie_OriginalClss

  这个过程其实就是 NSObject 的 dealloc 方法所做的事。运行期系统如果发现 NSZombieEnable 环境变量已设置,那么就把 dealloc 方法 “调配” (swizzle, 参见第13条)成一个会执行上述代码的版本。执行到程序末尾时,对象所属的类已经变为 _NSZombie_OriginalClass 了,其中 OriginalClass 指的是原类名。

  代码中的关键之处在于:对象所占内存没有(通过调用free()方法)释放,因此,这块内存不可复用。虽说内存泄漏了,但这只是个调试手段,制作正式发行的应用程序时不会把这项功能打开,所以这种泄漏问题无关紧要。

  但是,系统为何要给每个变为僵尸的类都创建一个对应的新类呢?这是因为,给僵尸对象发消息后,系统可由此知道该对象原来所属的类。假如把所有僵尸对象都归到 _NSZombie_ 类里,那原来的类名就丢了。创建新类的工作由运行期函数 objc_duplicateClass() 来完成,它会把整个 _NSZombie_ 类结构拷贝一份,并赋予其新的名字。副本类的超类、实例变量及方法都和复制前相同。还有种做法也能保留旧类名,那就是不拷贝 _NSZombie_ ,而是创建继承自 _NSZombie_ 的新类,但是用相应的函数完成此功能,其效率不如直接拷贝高。

  僵尸类的作用会在消息转发例程(参见第 12 条)中体现出来。 _NSZombie_ 类(以及所有从该类拷贝出来的类)并未实现任何方法。此类没有超类,因此和 NSObject 一样,也是个“根类”,该类只有一个实例变量,叫做 isa ,所有 Objective-C 的根类都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过 “完整的消息转发机制”(full forwarding mechanism, 参见第 12 条)。

  在完整的消息转发机制中, __forwarding__ 是核心,调试程序时,大家可能在栈回溯消息里看见过这个函数。它首先要做的事情就包括检查接收消息的对象所属的类名。若名称前缀为 _NSZombie_,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息(本条目开头曾列出),其中指明了僵尸对象所收到的消息及原来所属的类,然后应用程序就终止了。在僵尸类名中嵌入原始类名的好处,这时就可以看出来了。只要把 _NSZombie_ 从僵尸类名的开头拿掉,剩下的就是原始类名。下列伪代码演示了这一过程:

 1   // Obtain the object's class
 2 
 3   Class cls = object_getClass(self);
 4 
 5 
 6 
 7   // Get the class's name
 8 
 9   const char *clsName = class_getName(cls);
10 
11 
12 
13   // Check if the class is prefixed with _NSZombie_ 
14 
15   if (string_has_prefix(clsName, "_NSZombie_")) {
16 
17     // If so, this object is a zombie
18     // Get the original class name by skipping past the 
19     // _NSZombie_ , i.e. taking the substring from character 10
20     const char *originalClsName = substring_from(clsName, 10);
21 
22 
23     // Get the selector name of the message
24     const char *selectorName = sel_getName(_cmd);
25   
26 
27     // Log a message to indicate which selector is 
28     // being sent to which zombie
29     Log(''*** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);
30     // Kill the application
31     abort();
32   }

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

  EOCClass *obj = [[EOCClass alloc] init];

  NSLog(@"Before 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 0x7fc821c02a00

  大家可以看到,这段消息明确指出了僵尸对象所收到的选择子及其原来所属的类,其中还包含接收消息的僵尸对象所对应的 “指针值”(pointer value)。在调试器中深入分析程序时,也许会收到此消息,而且若能与适当的工具(比如Xcode 自带的 Instruments)相搭配,则效果甚佳。

END

posted @ 2017-08-12 15:05  鳄鱼不怕牙医不怕  阅读(440)  评论(0编辑  收藏  举报