Core Data - undoing changes after saving a context - ios

I've recently noticed this strange thing about undo mechanism in Core Data and it's bothering me ever since.
A quote from NSManagedObjectContext documentation for -undo method:
Sends an undo message to the receiver’s undo manager, asking it to reverse the latest uncommitted changes applied to objects in the object graph.
To reverse the latest uncommitted changes, sounds simple, right?
However, it's not what is actually happening! Even if I save the context with changes on my managed object, the following -undo call will still successfully reverse the changes. Isn't it against the thing stated in the docs?
Perhaps I'm doing something wrong? I can post my little testing code if needed. I'm really confused.

You should be confused. The Core Data documentation is a hot mess. They use a lot of words like "uncommitted" in arguably inappropriate ways. They seem to mean objects whose properties isFaulted is equal to NO when they say "uncommitted".
The Core Data Programming guide goes into more detail:
Change and Undo Management
A context keeps strong references to managed objects that have pending
changes (insertions, deletions, or updates) until the context is sent
a save:, reset , rollback, or dealloc message, or the appropriate
number of undos to undo the change.
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.
The wording/vocabulary in the documentation is not clear or consistent. I believe the intended usage is that the you should call removeAllActions on the context's undoManager property when appropriate for your application to avoid unlimited memory growth.

Related

EXC_BAD_ACCESS on mergeChangesFromContextDidSaveNotification

We have been trying to debug a Core Data multiple-context/threading issue wherein merging a Core Data save notification into our main thread NSManagedObjectContext is sporadically crashing the app. This is crashing ~2% of our app sessions and we are at a loss as to how to solve this. We would really appreciate any guidance or general advice on what could possibly cause this crash.
We have a Core Data setup that looks like this:
N.B. This is the default Core Data stack in Magical Record v2.3 created from [MagicalRecord setupAutoMigratingCoreDataStack]
This is the scenario where our app is crashing:
HTTP request returns JSON
JSON is parsed into NSManagedObjects (Some new entities, some updated entities) on Root Saving Context
Root Saving Context saves to persistent store
NSManagedObjectContextDidSaveNotification is broadcast by Core Data. Default context on main queue observes this and calls mergeChangesFromContextDidSaveNotification: with the NSDictionary of changes on the main thread.
It crashes when objectID is sent to an invalid object (most likely NSManagedObject has been deallocated).
This is occurring inside the private implementation of NSManagedObjectContext mergeChangesFromContextDidSaveNotification: so it is impossible for us to see what has actually gone wrong here; all we can tell at this point that an object which should exist, does not.
This only happens on a small percent of Core Data saves, indicating that may not be a fundamental flaw in our Core Data → API stack. Moreover, there is no indication that the size or type of the changes (insertions/updates/deletions) in the context changes have any impact on the likelihood of the crash.
The documentation of NSManagedObjectContextDidSaveNotification says that:
"You can pass the notification object to mergeChangesFromContextDidSaveNotification: on another thread, however you must not use the managed object in the user info dictionary directly on another thread. For more details, see Concurrency with Core Data in Core Data Programming Guide."
Maybe this is the issue? I would make sure the object u get from the notification is getting saved on Default Context on the same thread it was posted by Root.
It's been some time now since this question was posted and after rediscovering it I'd like to answer my own question for the sake of others who find this thread.
In my circumstance I had migrated a large code base from sibling NSManagedObjectContexts updated via NSManagedObjectContextDidSaveNotification's. However the problem was not really anything to do with this, even though this did expose the issue.
The real cause of this were places that there were older parts of the code, setup by previous engineers, that had setup KVO on NSManagedObjects and their properties. It transpired that KVO on Core Data entities is in fact a very very bad idea.
More accurately, it appeared that this happened when KVO was setup on entities and either the object, or the target of a relationship on this object was deleted from the NSPersistentStore. This second condition seemed to not be the only cause of the issue, but was definitely a very prominent cause in my situation.
Lesson's learnt:
Use a fetched results controller when you need to. KVO is not a convenient shortcut and you shouldn't avoid migrating dodgy Core Data KVO code to NSFetchedResultsControllers or another sensible alternative as the procrastination will just hurt you.
Multi threaded Core Data is a difficult but very worthwhile skill to become an expert in. Knowing your Core Data stack and the nuances and limitations of Core Data multithreading is absolutely worth all the mental anguish.
One possibility is that your persistent store has become corrupted and is in an inconsistent state. If this happens an error code is generated which Magical Record does not necessarily deal with. This can be the source of a number of difficult-to-repeat apparently-random crashes related to Magical Record (and may or may not be considered a Magical Record bug).
It's worth reading the Magical Record issues threads here (same issue) and here (different issue, but could be similar cause). When I hit these problems I managed to make some temporary patch fixes following various hints in those threads, but ultimately I decided to remove my dependency on Magical Record, and I have had no problems since then.

Merging multiple child managed object contexts

In my iOS application, I am attempting to sync core data with a web back end. I want to use a separate background managed object context for the sync so I can keep my main context free to accept changes from the ui while the sync is processing. Both contexts are children of my write-to-disk context as per this blog post http://www.cocoanetics.com/2012/07/multi-context-coredata/.
My question is, how can I merge both children contexts before I save to disk?
If I subscribe to contextDidSaveNotifications, I can merge the contexts using
[mainContext mergeChangesFromContextDidSaveNotification:syncFinishedNotification];
but according to the documentation...
"This method refreshes any objects which have been updated in the other context, faults in any newly-inserted objects, and invokes deleteObject:: on those which have been deleted."
I don't want to refresh the updated objects and lose changes made to the mainContext, but rather merge both change sets.
I am new to multi-context core data, so I may be thinking of this in the wrong way.
Any ideas?
Merging changes in Core Data is always a process of taking changes in one managed object context and then applying them to another context. If both contexts might acquire new changes at the same time, the merge is affected by the context's merge policy. If there are no conflicting changes, there's nothing to worry about. If there might be, though, you'll need to choose an appropriate merge policy.
The default value if you do nothing is NSErrorMergePolicyType, which means that saving changes would fail after merging changes. You almost certainly don't want that. But there are other predefined policies to choose from. NSMergeByPropertyObjectTrumpMergePolicyType is often a good choice here, because gives priority to unsaved conflicting changes. So if the sync context makes conflicting changes to an object the user is editing, the user's changes are preserved. There are a few other canned options. If none of them fit, you can always subclass NSMergePolicy and do whatever you like. That's rarely necessary, though.

How to force NSUndoManager prepareWithInvocationTarget to retain it's arguments?

NSUndoManager method prepareWithInvocationTarget does not retain arguments. There is no links to this in Apple docs, but I've checked with profiler, and I'm pretty sure - it does not. This means if you are going to delete an object and be prepared for undo - you should retain it yourself (like assign it to some trash array and remove the original link). Those kind of fake removes creates a lot of unnecessary fuss, especially when you need to get rid of the old undos.
However, NSInvocation can retain arguments by calling retainArguments method. Since NSUndoManager uses NSInvocation for prepareWithInvocationTarget it might be the way to pass retainArguments to NSUndoManager.
The question is - how to do it?
It's right there in the Undo Architecture guide:
An NSUndoManager object does not retain the targets of undo
operations. The client—the object performing undo operations—typically
owns the undo manager, so if the undo manager in turn retained its
target this would frequently create a retain cycle.
So No, it doesn't - for the reasons given in the docs.
You'll have to manage live cycles on your own. How entirely depends on and is completely specific to your apps model.. I guess the general question boils down to:
How do we know when the NSUndoManager will 'pop' an action from the undo stack (so we can safely delete our context data we're keeping around for the undo operation).
Unfortunately there's no (documented) notification in the NSUndoManager Class Reference that's sent e.g. when removeAllActionsWithTarget: is invoked on an undo manager instance.
Almost seems like there's no real alternative to keep that additional information around forever (or manually limit the levels of undo and adjust the storage for the undo context data accordingly).
While the answer that was given is correct with respect to the target, the answer with respect to the arguments is: There's no need to retain them, because the undo manager does it. See Does NSUndoManager retain its arguments?

Can you edit the same NSManagedObject in 2 different ManagedObjectContexts and merge their changes?

I'm syncing with a MySQL database.
Initially, I was going to loop through all my new/modified objects and set all the foreign keys for that object and then do the next object, and so on... But that's a lot of fetch requests.
So instead I wanted to loop through all my new/modified objects and set the foreign keys one at a time. So the first pass over my objects sets fk1, my next sets fk2, so on...
Cool, fetch requests drastically reduced. Now I'm curious if I could thread these fk setters. They aren't dependent on each other, but they are modifying the same object, even though they're only setting one relationship, and it's a different relationship. Speaking in git terms, these changes could be 'merged' together without any conflict, but is it possible to push changes in one child managedObjectContext(childContext:save) up to the parentManagedObjectContext(parent:performBlock^{parent:save}) and pull it down in another, different child managedObjectContext(???)? Or will the merge policy only take one childContext's version of the object and leave the other fks effectively unchanged.
I know this exists: NSManagedObjectContext/refreshObject:mergeChanges:
But that's on an object by object level. Will that cause a bunch of fetches? Or will that update my whole context at once/in batches?
Following Apple's suggestion from here:
https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CoreData/Articles/cdImporting.html
I've created/updated my values before I start setting any relationships, so all entities already exist before I try to point any relationships at them.
Aside: We have a couple apps that could benefit from the concurrency, because they throw a considerable amount of data around, and with the quad core iPad apps, this would really help out with the time the initial sync takes.
I'n not sure what you are trying to do and why (you could write less lines in your question and be more clear), but here are some guidelines for working with Core Data:
-- NSManagedObjectContext is not thread safe. Therefore, you need to limit your access to this managed object context to happen inside 1 thread. Otherwise, you may end up having many errors you can't understand.
-- NSManagedObjectContexts apart from doing other things, serve like "snapshots" of your persistent store. That means that when you change an object, you save it to the persistent store, you post a NSManagedObjectContextDidSaveNotification and then call mergeChangesFromContextDidSaveNotification: in another place inside your program in order to load the latest data from the persistent store. Be careful of thread safety.
NSManagedObjectContext/refreshObject:mergeChanges: according to apple is not about just refreshing a managed object. If you pass YES as the second argument, it will write any pending changes from this managed object context to the persistent store, and will load any other changes to other properties of this object from the persistent store, thus "synchronizing" your object with the persistent store. If you pass NO as the second argument, the object loses any pending changes, and it is being turned to a fault. That means that when you attempt to access it, the Managed Object Context will reload the object as it was last saved to the database. It will NOT reload the entire managed object context. It will only operate on the object.
Aside: I have written a blog post that scratches the surface of asynchronous loading from a core data database. In my case, since I'm doing heavy lifting with the database, I ended up using an NSOperation that operates with its own NSManagedObjectContext, and using serial GCD queues to save large chunks of data, since it was faster than having multiple threads accessing the same persistent store, even if they operate on different managed object contexts.
I hope I helped.

Avoiding registered object buildup (memleak) in NSManagedObjectContext

I have a memory-intensive iOS app and I'm working on making sure that memory usage does not build up over time. My app has a "main" context that lives for the lifetime of the app, with other smaller contexts being spawned occasionally for background tasks.
One thing I have noticed is that NSManagedObjects appear to remain registered in the main context long-term and the only way to truly reclaim all the memory associated with pulling the objects from the DB is to call [NSManagedObjectContext reset].
This of course results in a nice drop in memory usage as all the registered objects from recently closed list views are properly ejected from memory, however it is annoying because you have just invalidated every object that was registered in that context that you still have a reference to (i.e. objects that are refered to by views that are still open), and you now need to re-fetch all the these objects from the database to avoid exceptions for accessing an invalidated object.
Is this the only way of flushing out the registered object set from an NSManagedObjectContext, or is there a better way that successfully ejects all the registered objects you no longer have references to, but doesn't invalidate all the NSManagedObjects that are still alive?
NSManagedObjectContext has an internal row cache, and the only way you can clear that out is by resetting the context. If you're actually experiencing memory issues, a few things that may help are:
Managed objects retain their related objects. If you have managed object A that's referenced by a relationship from some other managed object B, then object A will remain in memory even if you've released all references to it. It won't actually be deallocated until object B is deallocated or re-faulted, because it will still be retained by B.
One way of dealing with this (and other memory issues) is by calling refreshObject:mergeChanges: on the MOC for objects you aren't currently using, with the second argument set to NO. That re-faults the object, i.e. makes the object go back to the initial "fault" state, unloading its property values and relationships. With A and B from before, re-faulting B would release the relationship to A. Keep in mind this will lose any unsaved changes on B, so make sure you've saved first if necessary.
If your managed objects contain any kind of large binary data, try moving that data into a separate entity or out of Core Data altogether to avoid loading it into memory when it's not needed.
reset on the MOC is basically the nuclear option when it comes to Core Data memory management. It's extremely effective but as you've found can be very dangerous. It's best avoided unless you already aren't using any objects loaded from the MOC.

Resources