One of the biggest issues with threading and Core Data has to do with thread blocking. No matter how cleverly you write the import and export operations, sooner or later you need to block the main queue to let the main NSManagedObjectContext talk to the NSPersistentStoreCoordinator and save changes out to disk.
Fortunately, this is a solvable problem and requires a small change to the Core Data stack. If you start your Core Data stack with a private queue, NSManagedObjectContext, and associate it with NSPersistentStoreCoordinator, you can have the main NSManagedObjectContext as a child of the private NSManagedObjectContext. Also, when the main NSManagedObjectContext is saved, it won’t produce a disk hit and will instead be nearly instantaneous. From there, whenever you want to write to disk you can kick off a save on the private contact and get asynchronous saves.
Adding this ability to the application requires a relatively small change. First, you need to add a property to the PPRDataController to hold onto the new private NSManagedObjectContext. Next, you tweak the -initializeCoreDataStack a little bit.
| NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"DataModel" |
| withExtension:@"momd"]; |
| NSAssert(modelURL != nil, @"Failed to find model URL"); |
| |
| NSManagedObjectModel *mom = nil; |
| mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; |
| |
| NSPersistentStoreCoordinator *psc = nil; |
| psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom]; |
| |
| NSManagedObjectContext *moc = nil; |
| NSManagedObjectContext *writer = nil; |
| NSUInteger type = NSPrivateQueueConcurrencyType; |
| |
| writer = [[NSManagedObjectContext alloc] initWithConcurrencyType:type]; |
| [writer setPersistentStoreCoordinator:psc]; |
| |
| type = NSMainQueueConcurrencyType; |
| moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:type]; |
| [moc setParentContext:writer]; |
| |
| [self setWriterContext:writer]; |
| [self setManagedObjectContext:moc]; |
Before, we had one NSManagedObjectContext configured to be on the main queue and writing to the NSPersistentStoreCoordinator. Now we’ve added a new NSManagedObjectContext that’s of type NSPricateQueueConcurrencyType. We set the NSPersistentStoreCoordinator to that private queue. Finally, we construct our main queue, NSManagedObjectContxt. Instead of handing off the NSPersistentStoreCoordinator to the main context, we give it to the parent: the private queue context.
With that change, any saves on the main NSManagedObjectContext will push up the changes only to the private queue NSManagedObjectContext. No writing to the NSPersistentStoreCoordinator occurs. However, there are times when we really do want to write to disk and persist our data changes. In that case, a couple of other changes are in order.
| - (void)saveContext |
| { |
| NSManagedObjectContext *moc = [self managedObjectContext]; |
| [moc performBlockAndWait:^{ |
| if (![moc hasChanges]) return; |
| NSError *error = nil; |
| if (![moc save:&error]) { |
| NSLog(@"Failed to save: %@\n%@", [error localizedDescription], |
| [error userInfo]); |
| abort(); |
| } |
| }]; |
| |
| moc = [self writerContext]; |
| [moc performBlock:^{ |
| if (![moc hasChanges]) return; |
| NSError *error = nil; |
| if (![moc save:&error]) { |
| NSLog(@"Failed to save: %@\n%@", [error localizedDescription], |
| [error userInfo]); |
| abort(); |
| } |
| }]; |
| } |
Previously in our -saveContext method, we checked to make sure we had an NSManagedObjectContext and that it had changes. In this new implementation, we first call -performBlockAndWait: on the main queue context and, inside that block, check to see if there are any changes. If there are no changes, then the block terminates. If there are changes, we call -save: on the main queue context and check for errors.
Once the main queue context has been saved (remember that -performBlockAndWait: blocks), we can kick off a save on the private queue context. Since we specifically want this save to be asynchronous, we call -performBlock:. Inside the -performBlock: the actions are the same. We check to see if there are any changes and, if there are, we call -save: and check for errors.
Because we use -performBlock: against the private queue context, the main queue is not blocked while data is being written out to disk. The application continues to perform with no delays in the user interface.