Search code examples
ios5core-datafaultretain-cycle

Core Data - break retain cycle of the parent context


Let's say we have two entities in a Core Data model: Departments and Employees.
The Department has a one-to-many relationship to Employees.

I have the following ManagedObjectContexts:
- Root: connected to the Persistent Store Coordinator
- Main: context with parent Root

When I want to create an Employee I do the following:
- I have a Department in the Main context
- I create an Employee in the Main context
- I assign the Department to the Employee's department property
- I save the Main context
- I save the Root context

This creates a retain cycle both in the Main context and in the Root context.

If I did this without a child context (all in the Root context), then I could break the retain cycle by calling refreshObject:mergeChanges on Employee. In my situation with the two contexts I could still use that method to break the cycle on the Main context, but how am I going to break the cycle on the Root context?

Side note: this is a simple example to describe my problem. In Instruments I can clearly see the number of allocations growing. In my app I have contexts that go deeper than one level, causing an even greater problem, because I get a new entity allocation with retain cycle per context I'm saving.

Update 15/04: NSPrivateQueueConcurrencyType vs NSMainQueueConcurrencyType
After saving both contexts I can perform refreshObject:mergeChanges on the Main context with the Department object. This will, as expected, re-fault the Department object, break the retain cycle and deallocate the Department and Employee entities in that context.

The next step is to break the retain cycle that exists in the Root context (saving the Main context has propagated the entities to the Root context). I can do the same trick here and use refreshObject:mergeChanges on the Root context with the Department object.

Weird thing is: this works fine when my Root context is created with NSMainQueueConcurrencyType (all allocations are re-faulted and dealloced), but doesn't work when my Root context is created with NSPrivateQueueConcurrencyType (all allocations are re-faulted, but not dealloced).

Side note: all operations for the Root context are done in a performBlock(AndWait) call

Update 15/04: Part 2
When I do another (useless, because there are no changes) save or rollback on the Root context with NSPrivateQueueConcurrencyType, the objects seem to be deallocated. I don't understand why this doesn't behave the same as NSMainQueueConcurrencyType.

Update 16/04: Demo project
I've created a demo project: http://codegazer.com/code/CoreDataTest.zip

Update 21/04: Getting there
Thank you Jody Hagings for your help!
I'm trying to move the refreshObject:mergeChanges out of my ManagedObject didSave methods.

Could you explain to me the difference between:

[rootContext performBlock:^{
    [rootContext save:nil];
    for (NSManagedObject *mo in rootContext.registeredObjects)
        [rootContext refreshObject:mo mergeChanges:NO];
}];

and

[rootContext performBlock:^{
    [rootContext save:nil];
    [rootContext performBlock:^{
        for (NSManagedObject *mo in rootContext.registeredObjects)
            [rootContext refreshObject:mo mergeChanges:NO];
    }];
}];

The top one doesn't deallocate the objects, the bottom one does.


Solution

  • I looked at your sample project. Kudos for posting.

    First, the behavior you are seeing is not a bug... at least not in Core Data. As you know, relationships cause retain cycles, that must be broken manually (documented here: https://developer.apple.com/library/mac/#documentation/cocoa/Conceptual/CoreData/Articles/cdMemory.html).

    Your code is doing this in didSave:. There may be better places to break the cycle, but that's a different matter.

    Note that you can easily see what objects are registered in a MOC by looking at the registeredObjects property.

    Your example, however, will never release the references in the root context, because processPendingEvents is never called on that MOC. Thus, the registered objects in the MOC will never be released.

    Core Data has a concept called a "User Event." By default, a "User Event" is properly wrapped in the main run loop.

    However, for MOCs not on the main thread, you are responsible for making sure user events are properly processed. See this documentation: http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html, specifically the last paragraph of the section titled Track Changes in Other Threads Using Notifications.

    When you call performBlock the block you give it is wrapped inside a complete user-event. However, this is not the case for performBlockAndWait. Thus, the private-context MOC will keep those objects in its registeredObjects collection until processPendingChanges is called.

    In your example, you can see the objects released if you either call processPendingChanges inside the performBlockAndWait or change it to performBlock. Either of these will make sure that the MOC completes the current user-event and removes the objects from the registeredObjects collection.

    Edit

    In response to your edit... It is not that the first one does not dealloc the objects. It's that the MOC still has the objects registered as faults. That happened after the save, during the same event. If you simply issue a no-op block [context performBlock:^{}] you will see the objects removed from the MOC.

    Thus, you don't need to worry about it because on the next operation for that MOC, the objects will be cleared. You should not have a long-running background MOC that is doing nothing anyway, so this really should not be a big deal to you.

    In general, you do not want to just refresh all objects. However, if you do you want to remove all objects after being saved, then your original concept, of doing it in didSave: is reasonable, as that happens during the save process. However, that will fault objects in all contexts (which you probably don't want). You probably only want this draconian approach for the background MOC. You could check object.managedObjectContext in the didSave: but that's not a good idea. Better would be to install a handler for the DidSave notification...

    id observer = [[NSNotificationCenter defaultCenter]
        addObserverForName:NSManagedObjectContextDidSaveNotification
                    object:rootContext
                     queue:nil
                usingBlock:^(NSNotification *note) {
        for (NSManagedObject *mo in rootContext.registeredObjects) {
            [rootContext refreshObject:mo mergeChanges:NO];
        }
    }];
    

    You will see that this probably gives you what you want... though only you can determine what you are really trying to accomplish.