views:

397

answers:

2

First, in my root view controllers viewDidLoad, I initialize an NSDictionary with arrays of NSManagedObjects, like so:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"Decks";

    UIBarButtonItem *browseButton = [[UIBarButtonItem alloc] initWithTitle:@"Browse" style:UIBarButtonItemStylePlain target:self action:@selector(loadBrowseView)];
    self.navigationItem.rightBarButtonItem = browseButton;
    [browseButton release];

    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
    }
    self.categories = [fetchedResultsController fetchedObjects];

    NSMutableDictionary *tipsMutableDictionary = [[NSMutableDictionary alloc] init];
    for (Category *category in self.categories) {
        NSMutableArray *tipsToSort = [NSMutableArray arrayWithArray:[[category valueForKey:@"tips"] allObjects]];

        NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending: YES];
        [tipsToSort sortUsingDescriptors: [NSArray arrayWithObject: sortDescriptor]];
        [sortDescriptor release];

        [tipsMutableDictionary setObject:[NSArray arrayWithArray:tipsToSort] forKey:[category name]];
    }
    self.tips = tipsMutableDictionary;
    [tipsMutableDictionary release];
}

Now "self.tips", which is a retained NSDictionary property on my root view controller, is set up to contain arrays of NSManagedObjects. I happily go along using that data for about 5 minutes (including passing it to other view's retained properties) until I get the following message:

malloc: *** error for object 0x3b300b0: double free

At this moment, after the error but before actually crashing, the backtrace is as follows:

#0  0x98152072 in malloc_error_break ()
#1  0x98153218 in szone_error ()
#2  0x9815334d in free_tiny_botch ()
#3  0x01b83064 in _PFDeallocateObject ()
#4  0x01b8d315 in -[_PFManagedObjectReferenceQueue _processReferenceQueue:] ()
#5  0x01bba9ba in _performRunLoopAction ()
#6  0x01d42252 in __CFRunLoopDoObservers ()
#7  0x01d4165f in CFRunLoopRunSpecific ()
#8  0x01d40c48 in CFRunLoopRunInMode ()
#9  0x0252d615 in GSEventRunModal ()
#10 0x0252d6da in GSEventRun ()
#11 0x002a3faf in UIApplicationMain ()
#12 0x00002830 in main (argc=1, argv=0xbfffef60) at /Users/***/main.m:14

Notably, there is no code of mine in there.

A print object on 0x3b300b0 reveals:

<Tip: 0x3b300b0> (entity: Tip; id: 0x3b2ffe0 <x-coredata://10B4E6EE-ACF3-4316-AE10-6E06E8FFFF46/Tip/p9> ; data: <fault>)

And most interestingly, the shell malloc_history on that address:

ALLOC 0x3b300b0-0x3b300ef [size=64]: thread_a0a3f500 |start | main | UIApplicationMain | -[UIApplication _run] | CFRunLoopRunInMode | CFRunLoopRunSpecific | PurpleEventCallback | _UIApplicationHandleEvent | -[UIApplication sendEvent:] | -[UIApplication handleEvent:withNewEvent:] | -[UIApplication _reportAppLaunchFinished] | CA::Transaction::commit() | CA::Context::commit_transaction(CA::Transaction*) | CALayerLayoutIfNeeded | -[CALayer layoutSublayers] | -[UILayoutContainerView layoutSubviews] | -[UINavigationController _startDeferredTransitionIfNeeded] | -[UINavigationController _startTransition:fromViewController:toViewController:] | -[UINavigationController _layoutViewController:] | -[UINavigationController _computeAndApplyScrollContentInsetDeltaForViewController:] | -[UIViewController contentScrollView] | -[UIViewController view] | -[RootViewController viewDidLoad] | -[_NSFaultingMutableSet allObjects] | -[_NSFaultingMutableSet willRead] | -[NSManagedObjectContext(_NSInternalAdditions) _retainedObjectWithID:optionalHandler:withInlineStorage:] | +[NSManagedObject(_PFDynamicAccessorsAndPropertySupport) allocWithEntity:] | _PFAllocateObject | malloc_zone_calloc 

----

FREE  0x3b300b0-0x3b300ef [size=64]: thread_a0a3f500 |start | main | UIApplicationMain | GSEventRun | GSEventRunModal | CFRunLoopRunInMode | CFRunLoopRunSpecific | __CFRunLoopDoObservers | _performRunLoopAction | -[_PFManagedObjectReferenceQueue _processReferenceQueue:] | -[NSManagedObjectContext(_NSInternalAdditions) _forgetObject:propagateToObjectStore:] | -[NSManagedObjectContext(_NSInternalAdditions) _forgetObject:propagateToObjectStore:removeFromRegistry:] | CFDictionaryRemoveValue | CFRelease | _PFDeallocateObject | malloc_zone_free 



ALLOC 0x3b300b0-0x3b300cf [size=32]: thread_a0a3f500 |start | main | UIApplicationMain | GSEventRun | GSEventRunModal | CFRunLoopRunInMode | CFRunLoopRunSpecific | __CFRunLoopDoObservers | _performRunLoopAction | -[_PFManagedObjectReferenceQueue _processReferenceQueue:] | _PFDeallocateObject | free_tiny_botch | szone_error | start | _NSPrintForDebugger | -[NSManagedObject description] | +[NSString stringWithFormat:] | -[NSPlaceholderString initWithFormat:locale:arguments:] | _CFStringCreateWithFormatAndArgumentsAux | CFStringCreateMutable | _CFRuntimeCreateInstance | malloc_zone_malloc 

My interpretation of the above is that "[RootViewController viewDidLoad]" in the first ALLOC statement is me initially populating the tips NSDictionary with data. Then in following FREE statement, main.m decides to free those objects even though they are in a retained NSDictionary that I have responsibility for releasing?

EDIT Per request, here is the NSZombieEnabled output:

*** -[CFString retain]: message sent to deallocated instance 0x3b300b0

Backtrace after that:

#0  0x01d6a3a7 in ___forwarding___ ()
#1  0x01d466c2 in __forwarding_prep_0___ ()
#2  0x01cfd988 in CFRetain ()
#3  0x01cfd495 in CFArrayCreate ()
#4  0x01d406c3 in -[__NSPlaceholderArray initWithObjects:count:] ()
#5  0x01d4f6f8 in -[NSArray initWithArray:copyItems:] ()
#6  0x01d5c408 in +[NSArray arrayWithArray:] ()
#7  0x0000392f in -[RootViewController getRandomTip:] (self=0x3e1db40, _cmd=0x745b, sender=0x3e27d60) at /Users/***/RootViewController.m:32
#8  0x00299405 in -[UIApplication sendAction:to:from:forEvent:] ()
#9  0x002fcb4e in -[UIControl sendAction:to:forEvent:] ()
#10 0x002fed6f in -[UIControl(Internal) _sendActionsForEvents:withEvent:] ()
#11 0x002fdabb in -[UIControl touchesEnded:withEvent:] ()
#12 0x002b2ddf in -[UIWindow _sendTouchesForEvent:] ()
#13 0x0029c7c8 in -[UIApplication sendEvent:] ()
#14 0x002a3061 in _UIApplicationHandleEvent ()
#15 0x0252ed59 in PurpleEventCallback ()
#16 0x01d41b80 in CFRunLoopRunSpecific ()
#17 0x01d40c48 in CFRunLoopRunInMode ()
#18 0x0252d615 in GSEventRunModal ()
#19 0x0252d6da in GSEventRun ()
#20 0x002a3faf in UIApplicationMain ()
#21 0x00002830 in main (argc=1, argv=0xbfffef60) at /Users/***/main.m:14

The referenced line of code from above (/Users/*/RootViewController.m:32) is:

NSArray *tipsArray = [NSArray arrayWithArray:[self.tips objectForKey:categoryNameString]];

All that I can tell from this is that the objects I am trying to access from "self.tips" have already been released (by who? and why?)

+1  A: 

Rerun your program with the environment variableNSZombieEnabled set to YES. That will tell you when you're double releasing the object, and what object it is. Alternatively, you can debug this in Instruments using the Zombies tool.

Dave DeLong
Howdy Dave, thanks for the response. As per your request I added the NSZombieEnabled output I got. I didn't see anything helpful there, but hopefully you (or another) will see something!
robenk
+1  A: 

self.tips hasn't been released; the Tip returned by [self.tips objectForKey:categoryNameString] has. It's returning a fault, not an object. Core Data will automatically fault objects when necessary.

Try putting this in when defining your NSFetchRequest:

[fetchRequest setReturnsObjectsAsFaults:NO];

This is what is going on: http://developer.apple.com/Mac/library/documentation/Cocoa/Conceptual/CoreData/Articles/cdFaultingUniquing.html, if you want to learn more about it.

Don
Awesome thanks a lot, going to look at that now. Although even if the "setReturnsObjectsAsFaults" flag fixes it, I am still wondering if I am misusing Core Data somehow (or just doing something non-standard). I've Googled these issue up, down and sideways, and there 's almost nothing out there. That seems odd for an issue where data which was once reliable becomes unreliable at a seemingly random time (also, in a way that is unintuitive with the standard cocoa memory management rules). Doing a fetch and then putting the data into a data structure doesn't seem rare...
robenk
Oy, unfortunately "setReturnsObjectsAsFaults:NO" doesn't seem to have fixed it. It still crashes in the same way, and the over released tip still says "data: <fault>". I put code like this to try and confirm the flag is working: [fetchRequest setReturnsObjectsAsFaults:NO];NSLog(@"RootView returnsFaults? %d", [fetchRequest returnsObjectsAsFaults]); That printed "RootView returnsFaults? 0", as it should. So shrug.
robenk
I realized afterward that setReturnsObjectsAsFaults: probably isn't what you want; that will retrieve the related objects instead of faults before they are accessed. It's the Tip itself that is being returned to a fault, probably due to memory constraints. Actually, I was going to ask why you are storing the results of the fetch. With Core Data, it seems like you should be using an NSPredicate to retrieve the data where you are using the NSDictionary
Don
I think you are onto the root of the problem, the idea of the tip faulting due to memory constraints sounds accurate to me. Re your question about why storing results of the fetch: what I am doing now is (this is a simplification) one fetch request when the app launches and then putting the results of the request into the NSDictionary, which I then pass around to the other views. Correct me if I am wrong, but I think what you are saying is that what I should be doing is have one fetch request _per view_ that fetches just the objects that I need for that specific view (via an NSPredicate)?
robenk
If you know of a sample code application that has the arrangement you are talking about that would be helpful. All the examples I've looked at seem to have one fetch request at the beginning that is then used the populate one table view. Of course these are also shallow applications. The NSDictionary was answer to "well what if I have more table views than that and each needs different data?"
robenk
"I think what you are saying is that what I should be doing is have one fetch request per view that fetches just the objects that I need for that specific view" Yes, that's it exactly. You would pass the NSManagedObjectContext to the view when you push the view controller. Then, the view controller would use its own NSFetchRequest to populate the table. So, the examples you see are correct, but the answer to your question, "what if each table needs different data" is that each table uses a separate fetch. It's really cool how easy it all works.
Don
Some of Apple's example code has the NSManagedObjectContext and NSManagedObjectModel for the whole app in the AppDelegate, like a Singleton, but you could also pass them to the view controllers that need them.
Don
Awesome, thanks a lot for your help Don. I am going to give this a shot. It will take awhile before I'll know definitively whether it solved the issue (some stuff to rewrite, and I have some other projects that need attention) -- so for the sake of this question I am going to assume it'll work and select this as the answer.
robenk
I rewrote the app to use the Core Data implementation described in this comment thread and that fixed the problem. Thanks a lot for your help! The comments not needing to use the NSDictionary and having one fetch per view were very helpful.
robenk