I'm confused by an occasional crash that I'm seeing, which, according to the Zombies instrument, is caused by the over-release of some dictionary values. When I look at the object history for one of these overreleased objects in Instruments, I see that its retain count drops straight from +2 to 0 at one stage. (Take a look at the screenshots at the end of the post). It's not clear to me how this is even possible.
I should say that I only see this crash when profiling with Instruments, so I suppose it could be an Apple bug, but it's probably safer to assume that it's pilot error, which Instruments is merely exposing.
Anyways, I'm constructing a CFDictionary that contains some Core Foundation objects (CFStrings and CFNumbers), and I then cast this to a NSDictionary* and pass it to an Objective-C method. A simplified version of my code is below:
// creates a CFDictionary containing some CFStrings and CFNumbers
void doStuff()
{
CFDictionaryRef myDict = CreateMyDictionaryContainingCFTypes();
dispatch_async(myQueue, ^{
[someObject receiveDictionary:(NSDictionary*)myDict];
CFRelease(myDict); // this line causes a crash. The Zombies instrument
// claims a CFString value contained in this
// dictionary has already been freed.
});
}
// ...
- (void)receiveDictionary:(NSDictionary*)dict
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSString* str1 = [dict objectForKey:@"key1"];
NSString* str2 = [dict objectForKey:@"key2"];
NSNumber* num1 = [dict objectForKey:@"key3"];
dispatch_async(myOtherQueue, ^{
[database executeUpdate:@"INSERT INTO blah (x,y,z) VALUES (?, ?, ?)", str1, str2, num1];
});
[pool drain];
}
I had thought that str1
, str2
and num1
would be seen as Objective-C objects, and would therefore be captured and automatically retained when the block in -receiveDictionary:
is copied by the dispatch_async
call, and released when that block is released. Indeed, these variables do seem to be captured and retained by the block. However, examining the object history for an overreleased CFString in Instruments, I can see that its reference count is being incremented when the block is copied. Perplexingly, its retain count drops from +2 straight to 0 when a block is released (see the screenshot at the end of the post); I don't know how to tell from the stack trace which block this is. By the time CFRelease
is called on the dictionary in the block in doStuff()
, some of its values have been deallocated already, and the program crashes.
So where did the extra release call come from? How can an object's retain count drop straight from +2 to 0, as Instruments indicates?
On a whim, I forced the second block to retain the entire dictionary, like so:
dispatch_async(myOtherQueue, ^{
[database executeUpdate:@"INSERT INTO blah (x,y,z) VALUES (?, ?, ?)", str1, str2, num1];
[dict self];
});
This seems to make the crash disappear; instruments stops reporting zombies, at least. I cannot for the life of me see why this works, though; surely I just to ensure that the block retains the dictionary values that I'm interested in, not the entire dictionary. So what's going on?
Instruments lists the following object history for the zombie CFString, with the object's retain count. I've included screenshots for the interesting events.
#0 +1 CFString is created
#1 +2 CFString added to dictionary
#2 +1 CFString released
#3 +2 CFString is retained when the block in -receiveDictionary:
is copied
#4 +0 What the...? The object's retain count dropped straight from +2 to 0!
#5 -1 CFDictionary is released, causing crash
CreateMyDictionaryContainingCFTypes()
. – Basilius[NSAutoreleasePool new]
? – Venous+[NSObject new]
is a method that returns the result of[[self alloc] init]
. In other words, it's just a convenience method. It's most frequently used with NSAutoreleasePool, mostly just out of habit though. – Basilius