Core Data - break retain cycle of the parent context
Asked Answered
B

3

14

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.

Bilski answered 14/4, 2013 at 14:4 Comment(4)
interesting question. In which context is the Department entity when you assign it to the Employee's department property?Archaeological
The Department is in the Main contextBilski
Do you have a small test case of code to demonstrate this? How are you saving the root context? Also, what do you see when you dump registeredObjects? Remember, performBlock wraps a complete "user event" but performBlockAndWait does not.Anytime
@JodyHagins I've added a demo project to illustrate some of the problems.Bilski
A
10

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.

Anytime answered 20/4, 2013 at 18:21 Comment(3)
Great, thank you very much for your explanation! I knew I had to break the retain cycles with refreshObject:mergeChanges but I didn't know about the User Events. I've added a small question to my opening post about breaking the retain cycle, could you explain the difference to me? Is that somehow caused by User Events as well?Bilski
Thanks again! I'll explore the notification approach in my app. If you ever decide to write a book on Core Data please let me know :)Bilski
Jody, I've ran into a possible Core Data bug. I consider you the Core Data master :), could you perhaps have a look: #31806178Bilski
R
3

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.

Rickart answered 15/4, 2013 at 5:55 Comment(5)
After propagation to the Root context the retain cycles will also exist in the Root context. I've tested this with instruments. I've updated the question with more info. I'm not using an undo manager (iOS default).Bilski
Fair enough, I will do my own experiments later when I have more time. I'll keep you posted about my findings.Rickart
@Zyphrax Posted an update with my findings. I don't have good news, it seems you'll have to settle with workarounds unless someone has a better explanation.Rickart
Thank you. I might file a bug report with Apple, I'm also not quite sure if this is the intended behavior of the framework. For now I'm using refreshObject:mergeChanges in didSave in the MO to trigger re-faulting in all MOCs (and eventually deallocation). Not pretty but it might suffice.Bilski
+1. Great research on the issue. If you've filed a bug report with Apple please post the bug # here so we can reference it when filing similar bugs.Annecorinne
W
1

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.

Wellrounded answered 14/4, 2013 at 20:45 Comment(5)
Using -reset is a bit of a sledgehammer approach that I cannot use. It will invalidate all entities living in my root context (not just re-fault them, but render them completely unusable). This would make the whole context-parentcontext system useless.Bilski
If you have objects living in you parent context, all changes to them will be saved allog with the changes you introduced from the child context. in this case, you will have to save the parent context, and keep the objects there (you might want to refresh them manually). in any case once a context is reset or deallocated, it disown all registered objects and turn them into faults ==> break retain cycles. you might want to consider changes in a separate context and merge them later using notifications.Wellrounded
My Root context is used to display items on screen, observed by KVO, there are quite a few active ManagedObjects. When I -reset the Root context all these objects become unusable. I'd have to reinitialize whole parts of my app to get them back. I simply can't imagine that this is how the parent MOC - child MOC is supposed to work. I don't understand why there aren't many more people experiencing this problem. Basically relationships + parent/child MOC = memory issues.Bilski
if you have memory issues when saving to a parent context, you must take care of the object graph yourself. after an object was faulted to a context and was added to the registered objects, he will remain there at least until the next save/reset, refreshing the object will only turn him into a fault and will not deallocate his stub memoryWellrounded
The NSManagedObjectContext has a strong reference to objects when they have changes, but if they don't have changes (after save), the reference is weak. Refreshing the object should therefor break the retain cycle, dropping the reference count to 0 and dealloc the object.Bilski

© 2022 - 2024 — McMap. All rights reserved.