The main objects of saving data in the background

What I'm trying to do in a nutshell, I use a background queue to save JSON objects pulled from a web service into a Core Data Sqlite3 database. Saving occurs in a serialized background queue created using GCD and is saved in the secondary instance of NSManagedObjectContext that is created for this background queue. After the save is complete, I need to update the NSManagedObjectContext instance, which is in the main thread with the newly created / updated objects. The problem I'm experiencing is an instance of NSManagedObjectContext in the main thread that cannot find objects that were stored in the background context. Below is a list of the actions that I take with the code samples. Any thoughts on what I'm doing wrong?

  • Create a background queue via GCD, run all the preprocessing logic, and then save the background context in this thread:

.

// process in the background queue dispatch_async(backgroundQueue, ^(void){ if (savedObjectIDs.count > 0) { [savedObjectIDs removeAllObjects]; } if (savedObjectClass) { savedObjectClass = nil; } // set the thead name NSThread *currentThread = [NSThread currentThread]; [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]; // if there is not already a background context, then create one if (!_backgroundQueueManagedObjectContext) { NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init]; [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator]; } } // save the JSON dictionary starting at the upper most level of the key path, and return all created/updated objects in an array NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0]; // save the object IDs and the completion block to global variables so we can access them after the save if (objectIds) { [savedObjectIDs addObjectsFromArray:objectIds]; } if (completion) { saveCompletionBlock = completion; } if (managedObjectClass) { savedObjectClass = managedObjectClass; } // save all changes object context [self saveManagedObjectContext]; }); 
  • The saveManagedObjectContext method basically examines which thread is running and saves the appropriate context. I checked that this method works correctly, so I will not post the code here.

  • All this code is in singleton mode, and in the singleton init method I add a listener for "NSManagedObjectContextDidSaveNotification" and call the mergeChangesFromContextDidSaveNotification method:

.

 // merge changes from the context did save notification to the main context - (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification { NSThread *currentThread = [NSThread currentThread]; if ([currentThread.name isEqual:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]) { // merge changes to the primary context, and wait for the action to complete on the main thread [_managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES]; // on the main thread fetch all new data and call the completion block dispatch_async(dispatch_get_main_queue(), ^{ // get objects from the database NSMutableArray *objects = [[NSMutableArray alloc] init]; for (id objectID in savedObjectIDs) { NSError *error; id object = [_managedObjectContext existingObjectWithID:objectID error:&error]; if (error) { [self logError:error]; } else if (object) { [objects addObject:object]; } } // remove all saved object IDs from the array [savedObjectIDs removeAllObjects]; savedObjectClass = nil; // call the completion block //completion(objects); saveCompletionBlock(objects); // clear the saved completion block saveCompletionBlock = nil; }); } } 

As you can see in the above method, I call "mergeChangesFromContextDidSaveNotification:" in the main thread, and I decided that the action would wait until completion. According to Apple's documentation, the background thread must wait until this action is complete before it can continue with the rest of the code below this call. As I mentioned above, I run this code, everything works, but when I try to print the selected objects to the console, I get nothing. It seems like the merge does not actually happen or may not end before my code ends. Is there another notice I should listen to make sure the merge is complete? Or do I need to keep the main context of the object after merging, but before fecth?

In addition, I apologize for the incorrect formatting of the code, but it seems that SO code tags do not like method definitions.

Thanks guys!

UPDATE:

I made the changes that were recommended below, but still have the same problem. Below is the updated code.

This is the code that causes processes to save background flow.

 // process in the background queue dispatch_async(backgroundQueue, ^(void){ if (savedObjectIDs.count > 0) { [savedObjectIDs removeAllObjects]; } if (savedObjectClass) { savedObjectClass = nil; } // set the thead name NSThread *currentThread = [NSThread currentThread]; [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]; // if there is not already a background context, then create one if (!_backgroundQueueManagedObjectContext) { NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init]; [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator]; } } // save the JSON dictionary starting at the upper most level of the key path NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0]; // save the object IDs and the completion block to global variables so we can access them after the save if (objectIds) { [savedObjectIDs addObjectsFromArray:objectIds]; } if (completion) { saveCompletionBlock = completion; } if (managedObjectClass) { savedObjectClass = managedObjectClass; } // listen for the merge changes from context did save notification [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext]; // save all changes object context [self saveManagedObjectContext]; }); 

This is the code called by the NSManagedObjectContextDidSaveNotification notification

  // merge changes from the context did save notification to the main context - (void)mergeChangesFromBackground:(NSNotification *)notification { // kill the listener [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext]; NSThread *currentThread = [NSThread currentThread]; // merge changes to the primary context, and wait for the action to complete on the main thread [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES]; // dispatch the completion block dispatch_async(dispatch_get_main_queue(), ^{ // get objects from the database NSMutableArray *objects = [[NSMutableArray alloc] init]; for (id objectID in savedObjectIDs) { NSError *error; id object = [[self managedObjectContext] existingObjectWithID:objectID error:&error]; if (error) { [self logError:error]; } else if (object) { [objects addObject:object]; } } // remove all saved object IDs from the array [savedObjectIDs removeAllObjects]; savedObjectClass = nil; // call the completion block //completion(objects); saveCompletionBlock(objects); // clear the saved completion block saveCompletionBlock = nil; }); } 

UPDATE:

So, I have found a solution. It turns out that the way I saved the identifiers of objects in the background thread, and then tried to use them in the main thread to retrieve them again, did not work. So I ended up pulling the inserted / updated objects from the userInfo dictionary, which is sent with an NSManagedObjectContextDidSaveNotification notification. Below is my updated code, which now works.

As before, this code triggers the pre-warning and save logic

 // process in the background queue dispatch_async(backgroundQueue, ^(void){ // set the thead name NSThread *currentThread = [NSThread currentThread]; [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]; [self logMessage:[NSString stringWithFormat:@"(%@) saveJSONObjects:objectMapping:class:completion:", [managedObjectClass description]]]; // if there is not already a background context, then create one if (!_backgroundQueueManagedObjectContext) { NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init]; [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator]; } } // save the JSON dictionary starting at the upper most level of the key path [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0]; // save the object IDs and the completion block to global variables so we can access them after the save if (completion) { saveCompletionBlock = completion; } // listen for the merge changes from context did save notification [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext]; // save all changes object context [self saveManagedObjectContext]; }); 

This is a modified method that processes NSManagedObjectContextDidSaveNotification.

 - (void)mergeChangesFromBackground:(NSNotification *)notification { // kill the listener [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext]; // merge changes to the primary context, and wait for the action to complete on the main thread [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES]; // dispatch the completion block dispatch_async(dispatch_get_main_queue(), ^{ // pull the objects that were saved from the notification so we can get them on the main thread MOC NSDictionary *userInfo = [notification userInfo]; NSMutableArray *modifiedObjects = [[NSMutableArray alloc] init]; NSSet *insertedObject = (NSSet *)[userInfo objectForKey:@"inserted"]; NSSet *updatedObject = (NSSet *)[userInfo objectForKey:@"updated"]; if (insertedObject && insertedObject.count > 0) { [modifiedObjects addObjectsFromArray:[insertedObject allObjects]]; } if (updatedObject && updatedObject.count > 0) { [modifiedObjects addObjectsFromArray:[updatedObject allObjects]]; } NSMutableArray *objects = [[NSMutableArray alloc] init]; // iterate through the updated objects and find them in the main thread MOC for (NSManagedObject *object in modifiedObjects) { NSError *error; NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error]; if (error) { [self logError:error]; } if (obj) { [objects addObject:obj]; } } modifiedObjects = nil; // call the completion block saveCompletionBlock(objects); // clear the saved completion block saveCompletionBlock = nil; }); } 
+6
source share
3 answers

in your case, because your letter to the background moc notification for mergeChangesFromContextDidSaveNotification will go into the background moc, not the foreground moc.

so you will need to register for notifications in the background thread going to the background moc object.

when you receive this call, you can send a message to the main moc thread for mergeChangesFromContextDidSaveNotification.

Andrew

update: here is a sample that should work

  //register for this on the background thread NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundMOC]; - (void)mergeChanges:(NSNotification *)notification { NSManagedObjectContext *mainThreadMOC = [singleton managedObjectContext]; //this tells the main thread moc to run on the main thread, and merge in the changes there [mainThreadMOC performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES]; } 
+4
source

I'm going to drop it there. Stop by following the guidelines for concurrency specified in the Master Data Programming Guide . Apple has not updated it since adding nested contexts, which are much easier to use. This video is fully revealed: https://developer.apple.com/videos/wwdc/2012/?id=214

Configure the main context to use the main thread (suitable for working with the user interface):

 NSManagedObjectContext * context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [context setPersistentStoreCoordinator:yourPSC]; 

For any object that you create that can perform concurrent operations, create a private queue context to use

 NSManagedObjectContext * backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; [backgroundContext setParentContext:context]; //Use backgroundContext to insert/update... //Then just save the context, it will automatically sync to your primary context [backgroundContext save:nil]; 

QueueConcurrencyType refers to the queue in which the context will perform the selection (saving and selecting the request). The NSMainQueueConcurrencyType context does everything in the main queue, which makes it suitable for interacting with the user interface. NSPrivateQueueConcurrencyType does this on its own private queue. Therefore, when you call save on backgroundContext, it combines the personal data that calls parentContext using performBlock , if necessary automatically. You do not want to call performBlock in the context of a private queue if this happens in the main thread, which causes a deadlock.

If you want to get really fantasy, you can create a primary context as a type of private concurrency queue (which is suitable for saving the background) with the main queue context only for your user interface, and then for the background contexts of your main queue, context for background operations (for example, import )

+24
source

I see you have developed an answer that works for you. But I had some similar problems, and I wanted to share my experience and see if it is really useful for you or other people who are looking at this situation.

The multi-threaded Core Data stuff is always a bit confusing to read, so please excuse me if I read your code incorrectly. But it seems that for you, there may be a simpler answer.

The main problem that you encountered first of all is that you saved the identifiers of the managed objects (presumably, the identifiers of the objects that can be transferred between streams), into a global variable for use in the main stream. You did this in the background thread. The problem was that you did this before you saved the context associated with the background thread. Object identifiers are unsafe for moving to another thread / context chain before saving. They may change when saved. See Warning in objectID documentation: NSManagedObject reference

You fixed this by notifying your background save stream and inside this stream, capturing the identifiers of the objects that were saved in use because they were saved in the object with the notification. They were transferred to the main thread, and the actual changes were also merged into the main thread with a call to mergeChangesFromContextDidSaveNotification. Here, where you can save one or two steps.

You register to listen for NSManagedObjectContextDidSaveNotification in the background thread. You can register to listen to the same notification in the main stream. And in this notification you will have the same object identifiers that are safe for use in the main thread. The main MOC stream can be safely updated using mergeChangesFromContextDidSaveNotification and the passed notification object, since the method is designed to work like this: mergeChanges docs . Calling the termination block from any thread is now safe as long as you agree with moc on the thread to which the termination block is called.

So, you can do all of your main streams by updating the material in the main stream, cleanly separating the streams and avoiding the need to pack and repack the updated materials or double, keeping the same changes in the permanent storage.

To be clear - the merge takes place on the contextand managed object and its state in memory - the moc in the main thread is updated in accordance with the one in the background thread, but a new save is not required, since you ALREADY saved these changes to the repository on the background thread. You have secure streaming access to any of these updated objects in the notification object, just like when used in a background thread.

I hope that your decision will work for you, and you do not have to refactor, but I would like to add my thoughts to those who can see it. Please let me know if I misinterpreted your code and I will fix it.

+6
source

Source: https://habr.com/ru/post/926764/


All Articles