Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 589
Zyphrax Avatar asked Apr 14 '13 14:04

Zyphrax


3 Answers

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.

like image 185
Jody Hagins Avatar answered Oct 19 '22 07:10

Jody Hagins


The steps you describe above are common tasks you perform in Core Data. And the side effects are clearly documented by Apple in Core Data Programming Guide: Object Lifetime Management.

When you have relationships between managed objects, each object maintains a strong reference to the object or objects to which it is related. This can cause strong reference cycles. To ensure that reference cycles are broken, when you're finished with an object you can use the managed object context method refreshObject:mergeChanges: to turn it into a fault.

The objects maintain strong references to each other only when they are not faults, but live instances NSManagedObject. With nested contexts, saving objects in your main context, those changes should be propagated to your root context. However, unless you fetch them in your root context, no retain cycles should be created. After you're done saving your main context, refreshing those objects should be all that is needed.

Regarding memory footprint in general:

If you feel the allocations are growing out of hand, you could try structure your code so that you perform tasks, that cause firing faults to a large amount of objects, in separate context which you discard when you're done with the task.

Also, if you are using undo manager,

The undo manager associated with a context keeps strong references to any changed managed objects. By default, in OS X the context’s undo manager keeps an unlimited undo/redo stack. To limit your application's memory footprint, you should make sure that you scrub (using removeAllActions) the context’s undo stack as and when appropriate. Unless you keep a strong reference to a context’s undo manager, it is deallocated with its context.

Update #1:

After experimenting with allocations instruments and piece of code written especially to test this, I can confirm that root context does not free up memory. Either this is a framework bug or it is intended by design. I found a post here describing the same issue.

Invoking [context reset] after [context save:] did release the memory as it should. Also I noticed, that prior to save, the root context had all the objects I inserted via child context in its [context insertedObjects] set. Iterating over them and doing [context refreshObject:mergeChanges:NO] did re-fault the objects.

So there seem to be few workarounds, but whether this is a bug and will be fixed in some upcoming release, or it will stay as it is by design, I do not know.

like image 45
svena Avatar answered Oct 19 '22 07:10

svena


When saving to the root context, the only one holding a strong reference to the objects is the root context itself, so, if you just reset it, the objects will be deallocated in the root context.
You save flow should be:
-save main
-save root
-reset root

I did not reset or refreshed the objects in the main context, and even though, no leaks or zombies were found. memory seems to be allocated and deallocated after the save and reset of the parent context.

like image 1
Dan Shelly Avatar answered Oct 19 '22 06:10

Dan Shelly