NSManagedObjectContext: performBlockAndWait vs performBlock with notification center - ios

I came across intriguing behaviour when using NSManagedObjectContext's performBlock: with notification center.
From the main UI thread I trigger asynchronous data download (using NSURLConnection's connectionWithRequest:). When data arrive the following delegate method is called:
- (void)downloadCompleted:(NSData *)data
{
NSArray *new_data = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
self.backgroundObjectContext = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.backgroundObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
[self.backgroundObjectContext performBlockAndWait:^{
[self saveToCoreData:new_data];
}];
}
The savetoCoreData: method is simply saving new data to the background context:
- (void)saveToCoreData:(NSArray*)questionsArray
{
for (NSDictionary *questionDictionaryObject in questionsArray) {
Question *newQuestion = [NSEntityDescription
insertNewObjectForEntityForName:#"Question"
inManagedObjectContext:self.backgroundObjectContext];
newQuestion.content = [questionDictionaryObject objectForKey:#"content"];
}
NSError *savingError = nil;
[self.backgroundObjectContext save:&savingError];
}
In the view controller, in viewDidLoad I add observer to the notification center:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextChanged:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
And then in the contexChanged: I merge the background context with the main context so that my NSFetchedResultsController's delegate methods are called where my view can be updated:
- (void)contextChanged:(NSNotification*)notification
{
if ([notification object] == self.managedObjectContext) return;
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
It all seems to work well, but there is one thing that bothers me. When in downloadCompleted: method I use performBlock: instead of performBlockAndWait: the notification seems to be delayed. It takes noticeable (around 5s) amount of time from the moment the background thread does save: till the moment NSFetchedResultsController calls its delegate. When I use performBlockAndWait: I do not observe any visible delay - the delegate is called as fast as if I called saveToCoreData: inside _dispatch_async_.
Does anyone saw that before and know if this is normal or am I abusing something?

As pointed out by Dan in one of the comments, the merge operation should happen on the main thread. This can be easily observed by changing the contextChanged: method to do the following:
- (void)contextChanged:(NSNotification*)notification
{
if ([notification object] == self.managedObjectContext) return;
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(contextChanged:)
withObject:notification
waitUntilDone:YES];
return;
}
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
With this change, both performBlock: and performBlockAndWait: are working.
As long as this explains to some extent why the problems were occurring in the first place, I still do not understand why performBlock: and performBlockAndWait: perform differently from the threading perspective. Apple documentation says:
performBlock: and performBlockAndWait: ensure the block operations are executed on the queue specified for the context. The performBlock: method returns immediately and the context executes the block methods on its own thread. With the performBlockAndWait: method, the context still executes the block methods on its own thread, but the method doesn’t return until the block is executed.
This indicates, that if the true root cause of the problem described in the question is that merging is happening in the background thread, then I should observe identical behaviour regardless of which method I am calling: performBlock: and performBlockAndWait: - both are executing in a sperate thread.
As a side note, since the NSURLConnection calls the delegate method on the same thread that started the asynchronous load operation - main thread in my case - using background context seems to be not necessary at all. NSURLConnections deliver its events in the run loop anyway.

Related

How to ignore changes in mergeChangesFromContextDidSaveNotification in NSManagedObjectContextWillSaveNotification

I am using a Private Managed Object Context to create some new objects into the persistent store, then after saving the private MOC, merging them into the main MOC using mergeChangesFromContextDidSaveNotification. This works fine, and updates the UI as required, and the NSManagedObjectContextWillSaveNotification is NOT invoked here for the mainMOC.
Then I make some changes to the mainMOC using the UI, and listen to NSManagedObjectContextWillSaveNotification. The notification is posted, but it contains not only the edits I made, but also the objects that were merged in from the PrivateMOC using mergeChangesFromContextDidSaveNotification.
Is there a way to ignore the changes that were merged in from another context into the mainContext, on subsequent contextDidChange notifications?
Here is the setup:
- (void) loadData {
privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType: NSPrivateQueueConcurrencyType];
privateContext.persistentStoreCoordinator = self.mainContext.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(contextWillSave:)
name:NSManagedObjectContextWillSaveNotification
object: self.mainContext];
NSManagedObject *object = [NSEntityDescription insertNewObjectForEntityForName:record.recordType inManagedObjectContext: self.privateContext];
// fill in object
if ([self.privateContext hasChanges]) {
[self savePrivateContextAndMergeWithMainContext: self.privateContext];
}
}
- (void) savePrivateContextAndMergeWithMainContext: (NSManagedObjectContext *) privateContext {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(privateContextDidChange:) name:NSManagedObjectContextDidSaveNotification object:privateContext];
__block NSError *error = nil;
[privateContext performBlockAndWait:^{
NSLog(#"PrivateContext saved");
[privateContext save:&error];
}];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:privateContext];
if (error) {
NSLog(#"error = %#", error);
}
}
- (void) privateContextDidChange: (NSNotification *) notification{
[self.mainContext performBlockAndWait:^{
NSLog(#"merged into mainContext");
[self.mainContext mergeChangesFromContextDidSaveNotification:notification];
}];
}
This works fine and saving the private context and merging into the mainContext doesn't trigger a contextWillSave notification. But on editing the data from the UI (on the main MOC) triggers the notification and includes the data that was previously saved using the private MOC.
Hope that's clear. Let me know if I should include anything else.
-- UPDATE --
Seems like the problem is with specifically deleting objects from the private context. After deleting from the private context, and calling mergeChangesFromContextDidSaveNotification on the main MOC, the mainMoc's deletedObjects set still shows the object that was deleted. This doesn't happen with inserts or updates in the private context. Is this documented anywhere? What could be the workaround?
Modifying privateContextDidChange like this ...
- (void) privateContextDidChange: (NSNotification *) notification{
if (notification.object == PrivateMOC) {
[self.mainContext performBlockAndWait:^{
NSLog(#"merged into mainContext");
[self.mainContext mergeChangesFromContextDidSaveNotification:notification];
}];
}
}
... where PrivateMOC is the reference to the Private Managed Object Context?
Core Data has been around for a number of years now, and over that time the concurrency model has evolved.
The "thread confinement" and "locking" concurrency models used notifications to communicate changes between contexts. When a context was saved a notification would be broadcast with information about the changes. That information could be used to merge the changes from one context into others. This never scaled particularly well and was often a major pain point for applications. The "locking" and "thread confinement" models have been obsolete for a number of years now.
When "queue confinement" was introduced the concept of "nested contexts" was introduced along with it. A context could have a parent context, and when the child context is saved those changes are automatically communicated to the parent (and no further). This is the recommended way to communicate changes between contexts when using queue confinement, which you are.
It is not recommended that you use mergeChangesFromContextDidSaveNotification: with a NSPrivateQueueConcurrencyType context.
Core Data performs a lot of internal bookkeeping and change tracking on your behalf. Normally during a save operation that information is aggregated and coalesced - this is part of the processPendingChanges operation. When the save operation is performed inside a performBlockAndWait: this may not happen, or may not be complete - which results in the changes (and any change notification) being incomplete or not happening at all. performBlockAndWait: is reentrant, which is not compatible with the processPendingChanges process. Using performBlock: instead will result in more consistent behavior.
If you use nested contexts to communicate changes rather than notifications your issue as you describe in your question will be solved.

How to notify main MOC, changes in background MOC

I am working with concurrency in coreData using threads, I followed sample example by apple, the link is here https://developer.apple.com/library/ios/samplecode/ThreadedCoreData/Introduction/Intro.html
I have few doubts regarding this sample project
I am using a class call PullOperation which is subClass of NSOperation, so when pull happens,
I want to notify my main MOC, about the changes so that It will be updated and shown on tableView
I want to know how to do this?
By following the sample app, I wrote this code in my appdelegate.m
- (NSManagedObjectContext *)managedObjectContext {
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_managedObjectContext = [NSManagedObjectContext new];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
// observe the ParseOperation's save operation with its managed object context
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
return _managedObjectContext;
}
// merge changes to main context,fetchedRequestController will automatically monitor the changes and update tableview.
- (void)updateMainContext:(NSNotification *)notification {
assert([NSThread isMainThread]);
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
// this is called via observing "NSManagedObjectContextDidSaveNotification" from our APLParseOperation
- (void)mergeChanges:(NSNotification *)notification {
if (notification.object != self.managedObjectContext) {
[self performSelectorOnMainThread:#selector(updateMainContext:) withObject:notification waitUntilDone:NO];
}
}
Here we can see there are notifications to merger changes.
But my app gets stuck and doesnot respond and I get message saying app stopped due to memory issues.
So I want to know where I am going wrong.
Please help
Regards
Ranjit
If your app halts when there is a managed object context change, I guess that is because both of your managed object context observe each other(by listening to NSManagedObjectContextDidSaveNotification), thus when there is a change, it will form an endless recursive call.
But I don't have enough code, so I'm just guessing, I suggest that you put a break point at this line:
[self performSelectorOnMainThread:#selector(updateMainContext:) withObject:notification waitUntilDone:NO];
and trigger a change to see if this line is entered many many times. If it is, then my guess is right.
EDIT:
By chatting, I got more information and the problem is because PO is using GAI(Google Analytics SDK for iOS), GAI uses core data for data persistent and GAI has its own core data stack, when GAI saves its context, it will post NSManagedObjectContextDidSaveNotification, and this notification goes globally, triggering -mergeChanges:, in -mergeChanges:, app will try to merge GAI's context with app's context, this two context is using different persistent store coordinator, which causes the problem.
To solve this, we need to check if the source context is using a same coordinator with the destination context, if not, no merging.

Merging MainContext blocks UI

I am working with Core Data and learning how things are done. I am currently reading all data from the main context and saving all data on the background context. I registered for notifications for when the background context is saved. When I merge the saved changes that were on the private context, the ui stops for split second. Is there any way to prevent that little glitch?
Here is what my context saved looks like:
#synchronized(self) {
[self.mainManagedObjectContext performBlock:^{
[self.mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
[[NSNotificationCenter defaultCenter] postNotificationName:ModelDidUpdateNotification
object:self.mainManagedObjectContext
userInfo:[notification userInfo]];
}];
}
I've tried taking out multiple lines of code, and the glitch stops when I take out the merge. But if I take that out. I don't get the changes I need on the main context.
Here is the background context save:
[self.backgroundMainManagedObjectContext performBlock:^
{
saveBlock(self.backgroundMainManagedObjectContext);
if([[NSThread currentThread] isMainThread])
{
NSLog(#"Saving on main thread!!");
}
if([self.backgroundMainManagedObjectContext hasChanges])
{
[self.backgroundMainManagedObjectContext save:&error];
}
}];
I don't really know what I'm doing as far as Core Data is concerned. So any help would be great!

CoreData: removal of 'didSave' notification immediately after save: call. Too soon?

-(void)someBackgroundTask {
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[context setPersistentStoreCoordinator:[self coordinator]];
// ...
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(handleSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:context];
[context save:&error];
// safe?
[notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:context];
// ...
}
// meanwhile on the main thread...
-(void)handleSaveNotification:(NSNotification *)notification {
[[self context] mergeChangesFromContextDidSaveNotification:notification];
}
Is is safe to remove the observer so soon after the call to save:?
As long as you've received the notification you want, it's not too soon. But there are other problems with that code.
It doesn't make any sense to add an observer, trigger the notification, and then remove the observer. You might as well call the handleSaveNotification method directly instead of bothering with notifications. It would have the same effect with less work.
The reason for this is that notifications are delivered synchronously on the thread they were posted on. So if someBackgroundTask is actually running on a background thread or queue, handleSaveNotification will also run in the background. Using notifications like this doesn't make you cross threads. To do the merge on the main thread, you have a few options, including:
Register for the notification using addObserverForName:object:queue:usingBlock:. With that method you can tell the notification center which queue to use. Make sure to save a reference to the object this method returns-- you'll need it later to remove the observer.
Call the merge method directly, but in that method use dispatch_async or performSelectorOnMainThread:withObject:waitUntilDone: to move the merge over to the main thread.

CoreData stack [child(background )/parent(thread 1 )] setup mergeChangesFromContextDidSaveNotification not updating UI(tableview)

quick question. I have a core data stack(child/parent)contexts.
The child fetches json objs and parses them then saves them up to the parent,and when the count is 20 parent gets main thread and saves ...All works fine. However in my tableview i end up having to refetch the whole db every time! My fetchcount and duration is huge do to this, can any one give me some ideas? Iam all out thanks in advance! Also for some reason [[[SharedStore ShareStoreManager]getMasterContext] reset] works fine ... just not mergeChangesFromContext!
NSNotificationCenter *mergeNotification = [NSNotificationCenter defaultCenter];
[mergeNotification addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:[[SharedStore ShareStoreManager]getMasterContext]]
-(void)mergeChanges:(NSNotification *)notification {
[[[SharedStore ShareStoreManager]getMasterContext] mergeChangesFromContextDidSaveNotification:notification];
[self.tableView layoutIfNeeded];
[self.tableView reloadData];
}
EDIT: I even went in to the context object and saw Inserted items that were not being merged so i went in there forced it but still no luck HELP!!!
for (User *user in [[notification.userInfo objectForKey:#"inserted"] allObjects]) {
[[[SharedStore ShareStoreManager]getMasterContext] refreshObject:user mergeChanges:YES];
}
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
I'd reconsider your design and use a NSFetchedResultsController to do the fetching of your objects. What's great about using that class for your data source is that it will automatically get notifications when things change in its managed object context. By implementing the delegate callbacks for it, you can have your tableview respond to changes in the fetched results controller's data by inserting, deleting, moving, modifying the appropriate rows in the table view.
Here's a tutorial outlining step by step how to hook it all up.
Edit:
Looking at your code, when you add the observer, you are only listening for saves that occur with your master context. If you are using a separate context to do your background processing, this notification will only get published for that background context, thus not triggering your observer. The only way your main context will trigger that notification is if you merge the background context with the main thread context and save the main thread context.
Your core data stack class should have its own observer, listening for all save events:
[[NSNotificationCenter defaultCenter] addObserver:sharedController
selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
It should then merge the changes occurring on different threads and contexts on the main thread:
- (void)contextDidSave:(NSNotification *)notification {
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self contextDidSave:notification];
});
} else {
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
}
Note here that we're making sure that we perform the merge on the main thread, since the main context was created on the main thread and is not thread safe. Once the merge is completed, notifications will go out to observers, such as an NSFetchedResultsController which will then trigger its own delegate callbacks, giving you the opportunity to refresh the UI.

Resources