Since objects created with or retrieved from a context can only be accessed on the queue associated with that context, the challenge becomes passing references to those objects between queues. This is arguably the biggest area where multiple threads with Core Data cause people the most issues.
If a reference to an object must be passed between queues, the best way to handle that hand-off is via the object’s -objectID property. This property is designed to be safe to access from multiple queues and is a unique identifier to the object.
Once you have a reference to the objectID associated with an NSManagedObject, you can retrieve another reference to that NSManagedObject from another context through a few methods:
-objectWithID: will return an object for any objectID passed to it. The danger with this method is that it’s guaranteed to return an object, even if it has to return an empty shell pointing to a nonexisting object. This can happen if an objectID is persisted and restored in a later application life cycle.
-existingObjectWithID: error: is a preferred method to use because it will give back an object if it exists and will return nil if no object exists for the objectID. The slight negative with this method is that it can perform I/O if the object isn’t cached.
-objectRegisteredForID: is the third option for object retrieval with objectID. This method will return the object if it’s already registered in the context that the method is being called against. Generally this method is only useful if you already know that the object has previously been fetched in the context.
In addition to object hand-off between queues (the passing of an object reference from one queue to another), there’s the handling of changes performed on a queue. By default, one context won’t notify another context if an object has been changed. It’s the responsibility of the developer to notify the other context of any changes. This is handled through the notification system.
Every time -save: is called against an NSManagedObjectContext, that context will broadcast a few notifications. The one that’s useful for cross-context notifications is NSManagedObjectContextDidSaveNotification. That notification is fired once the save has completed successfully, and the notification object that’s passed along includes all of the objects that were a part of the save.
If you have two contexts that you wish to keep in sync with each other, you can subscribe to this notification and then instruct the other context to consume the notification. For example, imagine that in your data controller you have two contexts—contextA and contextB—and you wish to keep them in sync. Once those contexts have been initialized, you can then subscribe to their notifications:
| NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; |
| [center addObserver:self |
| selector:@selector(contextASave:) |
| forKey:NSManagedObjectContextDidSaveNotification |
| object:contextA]; |
| [center addObserver:self |
| selector:@selector(contextBSave:) |
| forKey:NSManagedObjectContextDidSaveNotification |
| object:contextB]; |
In general you want notification observations to be as narrowly focused as possible. Although you could pass nil to the object: parameter, there would be no guarantee of who was broadcasting the notification, and you’d then need to filter inside the receiving method. By defining what objects we’re willing to accept notifications from, we don’t need to write defensive code in the receiving method and as a result we keep the receiving methods cleaner.
Once you see this notification, you can then consume it:
| - (void)contextASave:(NSNotification*)notification |
| { |
| [self.contextB performBlock:^{ |
| [self.contextB mergeChangesFromContextDidSaveNotification:notification]; |
| }]; |
| } |
| |
| - (void)contextBSave:(NSNotification*)notification |
| { |
| [self.contextA performBlock:^{ |
| [self.contextA mergeChangesFromContextDidSaveNotification:notification]; |
| }]; |
| } |
With this implementation, every time contextA is saved, contextB will be notified and every time contextB is saved, contextA will be notified. Note that these -mergeChangesFromContextDidSaveNotification: calls should be wrapped in a -performBlock: to guarantee that they’re being processed on the proper queue.
It should be noted that while the -mergeChangesFromContextDidSaveNotification: is being consumed, the context is also notifying any of its observers that changes are taking place. This means that there can be side effects to this call.
For example, if contextA has an NSFetchedResultsController associated with it and that NSFetchedResultsController has some expensive cell drawing associated with it, we can expect to see a performance hotspot while consuming notifications. The reason for that is that the processing of these notifications isn’t threaded and the call to -mergeChangesFromContextDidSaveNotification: won’t return until all of the cells associated with that NSFetchedResultsController have completed their processing. Worse, since NSFetchedResultsController and the associated cells are on the main queue, the entire application’s user interface is effectively halted while these changes are being processed. This can result in some surprising user interface delays. The best way to avoid these types of performance issues is to keep the cells from taking too long to draw or to break the save notification up into smaller units that can be processed faster.