Why is object not dealloc'ed when using ARC + NSZombieEnabled

Solution 1:

Turns out, I've written some serious nonsense

If zombies worked like I originally wrote, turning on zombies would directly lead to innumerable false positives...

There is some isa-swizzling going on, probably in _objc_rootRelease, so any override of dealloc should still be called with zombies enabled. The only thing that won't happen with zombies is the actual call to object_dispose — at least not by default.

What's funny is that, if you do a little logging, you will actually see that even with ARC enabled, your implementation of dealloc will call through to it's superclass's implementation.

I was actually assuming to not see this at all: since ARC generates these funky .cxx_destruct methods to dispose of any __strong ivars of a class, I was expecting to see this method call dealloc — if it's implemented.

Apparently, setting NSZombieEnabled to YES causes .cxx_destruct to not be called at all — at least that's what happened when I've edited your sample project:
zombies off leads to backtrace and both deallocs, while zombies on yields no backtrace and only one dealloc.

If you're interested, the additional logging is contained in a fork of the sample project — works by just running: there are two shared schemes for zombies on/off.


Original (nonsensical) answer:

This is not a bug, but a feature.

And it has nothing to do with ARC.

NSZombieEnabled basically swizzles dealloc for an implementation which, in turn, isa-swizzles that object's type to _NSZombie — a dummy class that blows up, as soon as you send any message to it. This is expected behavior and — if I'm not entirely mistaken — documented.

Solution 2:

This is a bug that has been acknowledged by Apple in Technical Q&A QA1758.

You can workaround on iOS 5 and OS X 10.7 by compiling this code into your app:

#import <objc/runtime.h>

@implementation NSObject (ARCZombie)

+ (void) load
{
    const char *NSZombieEnabled = getenv("NSZombieEnabled");
    if (NSZombieEnabled && tolower(NSZombieEnabled[0]) == 'y')
    {
        Method dealloc = class_getInstanceMethod(self, @selector(dealloc));
        Method arczombie_dealloc = class_getInstanceMethod(self, @selector(arczombie_dealloc));
        method_exchangeImplementations(dealloc, arczombie_dealloc);
    }
}

- (void) arczombie_dealloc
{
    Class aliveClass = object_getClass(self);
    [self arczombie_dealloc];
    Class zombieClass = object_getClass(self);

    object_setClass(self, aliveClass);
    objc_destructInstance(self);
    object_setClass(self, zombieClass);
}

@end

You will find more information about this workaround in my blog post Debugging with ARC and Zombies enabled.

Solution 3:

Turns out it is an iOS bug. Apple has contacted me and indicated they've fixed this in iOS 6.