Effective Objective-C 2.0: Item 35: Use Zombies to Help Debug Memory-Management Problems

来源:互联网 发布:淘宝怎么玩的 编辑:程序博客网 时间:2024/05/16 02:24

Item 35: Use Zombies to Help Debug Memory-Management Problems

Debugging memory-management issues can be painful. Sending a message to a deallocated object is completely unsafe, as one would expect. But sometimes it works, and sometimes it doesn’t. It all depends on whether the memory where the object used to reside has been overwritten. Whether or not the memory is reused is nondeterministic, so a crash may happen only occasionally. Other times, the memory will be only partially reused, so certain bits of the object are still valid. Yet other times, the memory will by sheer fluke have been overwritten with another valid, live object. In these cases, the runtime will send the message to the new object, to which it may or may not respond. If it does, the app won’t crash, but you’ll wonder why objects you didn’t expect to be receiving messages are. If it doesn’t respond to that selector, the application will usually crash.

Fortunately, Cocoa’s “zombies” feature can come in handy. When this debugging feature isenabled, the runtime turns all deallocated instances into a special zombie object rather than deallocating them. The core memory where the object is located is not made available for reuse; therefore, nothing will ever overwrite it. When it receives any message, a zombie object throws an exception saying exactly what message was sent and what the object used to be when it was still alive. Using zombies is the best way to debug memory-management problems.

The feature is turned on by setting the NSZombieEnabled environment variable to YES. For example, if you’re using bash and running an application on Mac OS X, you would do something like this:

export NSZombieEnabled="YES"
./app

When a message is sent to a zombie, a message will be printed to the console, and the application will terminate. The message will look like this:

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

It is also possible to turn on the option in Xcode such that the environment variable is automatically set when the application is run from within Xcode. To do this, you edit theapplication’s scheme, select the Run configuration, then the Diagnostics tab, and finally turn onEnable Zombie Objects. Figure 5.7 shows the dialog that you should see in Xcode, with the option to enable zombies turned on.

Image

Figure 5.7 Enabling zombie objects from Xcode’s scheme editor

So how does the zombies feature work? It is implemented deep within the Objective-C runtime and the Foundation and CoreFoundation frameworks. When an object is being deallocated, an additional step is made by using the environment variable if this feature is enabled. This extra step turns the object into a zombie rather than fully deallocating it.

To see what this extra step does, consider the following code:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface EOCClass : NSObject
@end

@implementation EOCClass
@end

void PrintClassInfo(id 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[]) {
    EOCClass *obj = [[EOCClass allocinit];
    NSLog(@"Before release:");
    PrintClassInfo(obj);

    [obj release];
    NSLog(@"After release:");
    PrintClassInfo(obj);
}

This code uses manual reference counting to make it easier to show what happens when an object becomes a zombie. ARC would ensure that the str object was alive for as long as it needed to be, meaning that it would never become a zombie in this simple scenario. That’s not to say that objects can never become zombies under ARC. This type of memory bug can still occur with ARC but usually manifests itself through slightly more complex code.

The code in the example has a function to print out the class and superclass names of a given object. The code uses object_getClass(), a runtime function, rather than sending the class Objective-C message. If the object is a zombie, sending any Objective-C message will cause the zombie error message to be printed out and the application to crash. The output of the code looks like this:

Before release:
=== EOCClass : NSObject ===
After release:
=== _NSZombie_EOCClass : nil ===

The object’s class has changed from EOCClass to _NSZombie_EOCClass. But where did this class come from? It hasn’t been defined in the code. Also, it would be fairly inefficient for the compiler to create an extra class for every class it finds, just in case zombies are enabled. What happens is that this _NSZombie_EOCClass is generated at runtime the first time an object of classEOCClass is turned into a zombie. This uses the powerful runtime functions that can manipulate the class list.

The zombie class is a duplicate of a template class called _NSZombie_. These zombie classes don’t do much but simply act as a marker. You’ll see how they act as a marker shortly. First, consider the following pseudocode showing how the zombie class is created if necessary and then how it is used to turn the deallocating object into a zombie.

// 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 exist,
// then it needs to be created
if (!zombieCls) {
    // Obtain the template zombie class called _NSZombie_
    Class baseZombieCls = objc_lookUpClass("_NSZombie_");

    // Duplicate the base 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

This routine is what becomes NSObject’s dealloc method. When it sees that theNSZombieEnabled environment variable is set, the runtime swizzles (see Item 13) the deallocmethod for a version that performs the preceding code. At the end of this routine, the class of the object has been changed to _NSZombie_OriginalClass, where OriginalClass is the name of the class that once was.

Crucially, the memory the object lives in is not freed (through a call to free()); therefore, the memory will not be available for use again. Although this is leaking memory, this is a debugging tool only and would never be turned on for production-running applications, so it doesn’t matter.

But why create a new class for each class that is turned into a zombie? This is done so that the original class can be determined when a message is sent to a zombie. If all zombies were of class _NSZombie_, the original class name would be lost. Creating a new class is done by using the runtime’s function objc_duplicateClass(), which copies the entire class but gives it a new name. The superclass, instance variables, and methods of the duplicate class will be identical to the one being copied. Another way to achieve the same goal of maintaining the old class name would be to create the new class as inheriting from _NSZombie_ rather than copying it. However, the functions to do this are less efficient than performing a direct copy.

The zombie class comes into action within the forwarding routines (see Item 12). The_NSZombie_ class (and therefore all its copies) do not implement any methods. The class does not have a superclass and is therefore a root class, just like NSObject, with a single instance variable, called isa, which all Objective-C root classes must have. This lightweight class does not implement any methods, so whenever it is sent any message, it will go through the full forwarding mechanism (see Item 12).

At the heart of the full forwarding mechanism is ___forwarding___, a function you may have seen in backtraces while debugging. One of the first things that this function does is check the name of the class of the object being sent a message. If this name is prefixed with _NSZombie_, a zombie has been detected, and something special happens. The application is killed at this point, after printing out a message (shown at the start of this item) to indicate what message was sent and to what type of class. That’s where the fact that the class name has the original class name within it comes in handy. The _NSZombie_ is removed from the start of the zombie class name to leave just the original name. Pseudocode showing what happens is as follows:

// 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 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();
}

The action of this routine can be seen if the example is extended to attempt to message the zombie EOCClass object:

EOCClass *obj = [[EOCClass allocinit];
NSLog(@"Before release:");
PrintClassInfo(obj);

[obj release];
NSLog(@"After release:");
PrintClassInfo(obj);

NSString *desc = [obj description];

If this is run with zombies enabled, the following is seen on the console:

Before release:
=== EOCClass : NSObject ===
After release:
=== _NSZombie_EOCClass : nil ===
*** -[EOCClass description]: message sent to deallocated
instance 0x7fc821c02a00

As you can see, this clearly shows what selector was sent and the original class of the object, as well as the pointer value of the dead object that was messaged. This information can be used if you are in a debugger for further analysis, if required, and can prove invaluable with the correct tools, such as Instruments, which ships with Xcode.

Things to Remember

Image When an object is deallocated, it can optionally be turned into a zombie instead of being deallocated. This feature is turned on by using the environment flag NSZombieEnabled.

Image An object is turned into a zombie by manipulating its isa pointer to change the object’s class to a special zombie class. A zombie class responds to all selectors by aborting the application after printing a message to indicate what message was sent to what object.

0 0
原创粉丝点击