Following this excellent post by Olivier Drobnik, I've implemented the three-layer CoreData stack proposed by CoreData guru Marcus S. Zarra:
The only difference from this diagram and my code is that I only use one Temporary Background MOC, in order to avoid duplicates when inserting objects in several temp MOCs. Here's my context initialisation code:
#pragma mark - NSManagedObjectContexts
+ (NSManagedObjectContext *)privateManagedObjectContext
{
if (!_privateManagedObjectContext) {
// Setup MOC attached to PSC
_privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateManagedObjectContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
// Add notification to perform save when the child is updated
_privateContextSaveObserver =
[[NSNotificationCenter defaultCenter]
addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSManagedObjectContext *savedContext = [note object];
if (savedContext.parentContext == _privateManagedObjectContext) {
[_privateManagedObjectContext performBlock:^{
NSLog(#"AMBCoreData -> saving privateMOC");
NSError *error;
if (![_privateManagedObjectContext save:&error]) {
NSLog(#"AMBCoreData -> error saving _privateMOC: %# %#", [error localizedDescription], [error userInfo]);
}
}];
}
}];
}
return _privateManagedObjectContext;
}
+ (NSManagedObjectContext *)mainUIManagedObjectContext
{
if (!_mainUIManagedObjectContext) {
// Setup MOC attached to parent privateMOC in main queue
_mainUIManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainUIManagedObjectContext setParentContext:[self privateManagedObjectContext]];
// Add notification to perform save when the child is updated
_mainUIContextSaveObserver =
[[NSNotificationCenter defaultCenter]
addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSManagedObjectContext *savedContext = [note object];
if (savedContext.parentContext == _mainUIManagedObjectContext) {
NSLog(#"AMBCoreData -> saving mainUIMOC");
[_mainUIManagedObjectContext performBlock:^{
NSError *error;
if (![_mainUIManagedObjectContext save:&error]) {
NSLog(#"AMBCoreData -> error saving mainUIMOC: %# %#", [error localizedDescription], [error userInfo]);
}
}];
}
}];
}
return _mainUIManagedObjectContext;
}
+ (NSManagedObjectContext *)importManagedObjectContext
{
if (!_importManagedObjectContext) {
// Setup MOC attached to parent mainUIMOC in private queue
_importManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_importManagedObjectContext setParentContext:[self mainUIManagedObjectContext]];
}
return _importManagedObjectContext;
}
This code is pretty straightforward. I'm replicating the above diagram using only the mainUIManagedObjectContext in the NSMainQueueConcurrencyType. Every time the child context, importManagedObjectContext gets saved, a notification is fired and all the parent contexts performs a save in it's current thread.
I've implemented a test view controller with a UITableView and a NSFetchedResultsController attached. This is the code in the viewDidLoad of my test view controller:
- (void)viewDidLoad
{
[super viewDidLoad];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Task"];
[request setSortDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"insertDate" ascending:NO]]];
self.fetchRequest = request;
NSFetchedResultsController *frc =
[[NSFetchedResultsController alloc]
initWithFetchRequest:self.fetchRequest
managedObjectContext:[AMBCoreData mainUIManagedObjectContext]
sectionNameKeyPath:nil
cacheName:nil];
frc.delegate = self;
[self setFetchedResultsController:frc];
[self.fetchedResultsController performFetch:nil];
}
Here I attach the mainUIManagedObjectContext to the NSFetchedResultsController. Later, in my viewDidAppear, I run a loop to insert a few Task entities:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[[AMBCoreData importManagedObjectContext] performBlock:^{
for (int i = 0; i < 5000; i++) {
Task *task = [NSEntityDescription insertNewObjectForEntityForName:#"Task" inManagedObjectContext:[AMBCoreData importManagedObjectContext]];
task.title = [NSString stringWithFormat:#"Task %d", i];
task.insertDate = [NSDate new];
[[AMBCoreData importManagedObjectContext] save:nil];
}];
}
The thing is, I'm inserting 5000 objects and the UI is freezing when the data is populated into the table view. Florian Kugler ran a test with this architecture inserting 15.000 objects and with instruments he got this main thread usage (blue is for main thread, gray for any other threads):
But here's my main thread CPU usage with 5000 objects, profiled using an iPhone 5:
As you can see, my main thread usage is far greater than Florian's and also my UI freezes for a few seconds. My question is, am I doing something wrong? Is this the expected behaviour when using this three-layer MOC architecture with a NSFetchedResultsController and a UITableView? I know that inserting 5000 objects is not the usual behaviour of most apps, so when I've tried with 50 or 100 objects the freeze was inexistent or unnoticeable, but the main thread usage was high (although I admit that in this case it can be due another reasons like waking up the app).
Yes, it is expected, because Main MOC is involved in the saves of its children. It is convenient and kind of okay when children of the UI context don’t do big saves, but often becomes a performance problem if those saves are bigger. You can’t be sure that the UI thread does only minimum job when using this pattern.
For the big saves I would recommend creating a context that is configured directly with the persistent store coordinator. After big save happens, you just refetch and optionally refresh data in the UI context. For more details see my answer here.
Related
SETUP (You can read this later and skip to the scenario section first)
It's an old app, with manually setup CoreData stack like this:
+ (NSManagedObjectContext *)masterManagedObjectContext
{
if (_masterManagedObjectContext) {
return _masterManagedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self createPersistentStoreCoordinator];
if (coordinator != nil) {
_masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_masterManagedObjectContext.retainsRegisteredObjects = YES;
_masterManagedObjectContext.mergePolicy = NSOverwriteMergePolicy;
_masterManagedObjectContext.persistentStoreCoordinator = coordinator;
}
return _masterManagedObjectContext;
}
+ (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext) {
return _managedObjectContext;
}
NSManagedObjectContext *masterContext = [self masterManagedObjectContext];
if (masterContext) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedObjectContext.retainsRegisteredObjects = YES;
_managedObjectContext.mergePolicy = NSOverwriteMergePolicy;
_managedObjectContext.parentContext = masterContext;
}
return _managedObjectContext;
}
+ (NSManagedObjectContext *)newManagedObjectContext
{
__block NSManagedObjectContext *newContext = nil;
NSManagedObjectContext *parentContext = [self managedObjectContext];
if (parentContext) {
newContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
newContext.parentContext = parentContext;
}
return newContext;
}
And then save context recursively:
+ (void)saveContext:(NSManagedObjectContext *)context
{
[context performBlockAndWait:^{
if (context.hasChanges && context.persistentStoreCoordinator.persistentStores.count) {
NSError *error = nil;
if ([context save:&error]) {
NSLog(#"saved context: %#", context);
// Recursive save parent context.
if (context.parentContext) [self saveContext:context.parentContext];
}
else {
// do some real error handling
NSLog(#"Could not save master context due to %#", error);
}
}
}];
}
SCENARIO
The app load lots of data from a server, then perform update inside newContext first, then merge into mainContext -> masterContext -> persistentStore.
Because lots of data, the sync process has been divided into about 10 async threads => we have 10 newContext at a time.
Now, the data is complicated, with things like parents <-> children (same class). 1 parent can have many children, and a child can have a mother, father, god father, step mother..., so it's n-n relationship. First, we fetch parent, then perform fetch child and then set the child to parent, and so on.
The server is kinda stupid, it can't send disabled objects. However the customer would like to control the display of app's objects from the back end, so I have 2 properties to do that:
hasUpdated: At the beginning of loading process, perform a batch update, set all object's hasUpdated to NO. When got data from the server, update this property to YES.
isActive: When all loading was done, perform batch update this property to NO if hasUpdate == NO. Then, I have a filter that won't show object with isActive == NO
ISSUE
Customers complain why some objects being missing even if they're enable in the backend. I've struggle and debugging for so long after got to this strange issue:
newContext.updatedObjects : { obj1.ID = 100, hasUpdated == YES }
"saved newContext"
mainContext.updatedObjects: {obj1.ID = 100, hasUpdated == NO }
// I'll stop here. Obviously, master got updated = NO and finally isActive will set to no, which cause missing objects.
If it happened every time, then probably easier to fix (¿maybe?). However, it occurs like this:
First time running (by first time, I mean app start from where appDidFinishLaunch... got called): all correct
2nd time: missing (153 objects)
3rd time: all correct
4th time: missing (153 objects) (again? exactly those with multiple parents, I believe so!)
5th time: correct again
... so on.
Also, it looks like this happened for objects which have the same context (same newContext). Unbelievable.
QUESTIONS
Why is this happening? How do I fix this? If those objects don't have children, my life would be easier!!!!
BONUS
In case you'd like to know how the batch update is, it's below. Note:
Download requests are in async queue: _shareInstance.apiQueue = dispatch_queue_create("product_request_queue", DISPATCH_QUEUE_CONCURRENT);
Parse response and update properties are syncronous in a queue: _shareInstance.saveQueue = dispatch_queue_create("product_save_queue", DISPATCH_QUEUE_SERIAL);
Whenever parse complete, I perform save newContext and call for updateProductActiveStatus: in the same serial queue. If all requests are finished, then perform batch update status. Since request are done in concurent queue, it's always finished earlier than save (serial) queue, so it's pretty much fool proof process.
Code:
// Load Manager
- (void)resetProductUpdatedStatus
{
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.propertiesToUpdate = #{ #"hasUpdated" : #(NO) };
request.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
NSLog(#"Batch update hasUpdated: %#", result.result);
[self.masterContext performBlockAndWait:^{
[self.masterContext refreshAllObjects];
[[CoreDataUtil managedObjectContext] performBlockAndWait:^{
[[CoreDataUtil managedObjectContext] refreshAllObjects];
}];
}];
}
- (void)updateProductActiveStatus:(SyncComplete)callback
{
if (self.apiRequestList.count) return;
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.predicate = [NSPredicate predicateWithFormat:#"hasUpdated = NO AND isActive = YES"];
request.propertiesToUpdate = #{ #"isActive" : #(NO) };
request.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
NSLog(#"Batch update isActive: %#", result.result);
[self.masterContext performBlockAndWait:^{
[self.masterContext refreshAllObjects];
NSManagedObjectContext *maincontext = [CoreDataUtil managedObjectContext];
NSLog(#"Refreshed master");
[maincontext performBlockAndWait:^{
[maincontext refreshAllObjects];
NSLog(#"Refreshed main");
// Callback
if (callback) dispatch_async(dispatch_get_main_queue(), ^{ callback(YES, nil); });
}];
}];
}
mergePolicy is evil. The only correct mergePolicy is NSErrorMergePolicy any other policy is asking core-data to silently fail and not update when you expect it too.
I suspect that your problem is that you are writing simultaneously to core-data with the background contexts. (I know that you say you have a serial queue - but if you call performBlock inside the queue then each block is executed simultaneously). When there is a conflict stuff gets overwritten. You should only write to core-data in one synchronous way.
I wrote an answer on how to accomplish this with a NSPersistentContainer:
NSPersistentContainer concurrency for saving to core data and I would suggest that you migrate your code to it. It really should not be that hard.
If you want to keep the code as close to what is currently is as possible that also is not that hard.
Make a serial operation queue:
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;
And do all writing using this queue:
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
void (^blockCopy)(NSManagedObjectContext*) = [block copy];
[self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSManagedObjectContext* context = [CoreDataUtil newManagedObjectContext];
[context performBlockAndWait:^{
blockCopy(context);
[CoreDataUtil saveContext:context];
}];
}]];
}
Also it could be that the objects ARE updated, but you aren't seeing it because you are relying on a fetchedResultsController to be updated. And fetchedResultsController don't update from batch update requests.
I just wanted to be clear about core data private context. I am trying to insert 20k record using private context (NSPrivateQueueConcurrencyType). But as soon as 'Insert 20k records' button is tapped UI thread hangs.
- (IBAction)insertRecords:(id)sender {
[[CoreDataStore privateContext] performBlock:^{
NSLog(#"Starting to insert 20k records...");
for (int i = 0 ; i < 20000; i++)
{
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:#"Event" inManagedObjectContext:[CoreDataStore privateContext]];
[newManagedObject setValue:[NSDate date] forKey:#"time"];
}
NSError * error = nil;
NSLog(#"Inserted 20k records to managed object context");
[[CoreDataStore privateContext] save:&error];
NSLog(#"Save context command fired");
}];
NSLog(#"returning from insert method");
}
changes from private context are being merged to main context here
- (void)contextDidSavePrivateQueueContext:(NSNotification *)notification
{
#synchronized(self) {
[self.mainContext performBlock:^{
NSLog(#"merging changes to main context.....");
[self.mainContext mergeChangesFromContextDidSaveNotification:notification];
NSLog(#"merged changes to main context");
}];
}
}
Below is log window output when I hit insert button:
2014-08-04 14:54:12.431 CoreDataDrillDown[11323:90b] returning from insert method
2014-08-04 14:54:12.431 CoreDataDrillDown[11323:1403] Starting to insert 20k records...
2014-08-04 14:54:12.506 CoreDataDrillDown[11323:1403] Inserted 20k records to managed object context
2014-08-04 14:54:12.785 CoreDataDrillDown[11323:90b] merging changes to main context.....
2014-08-04 14:54:12.786 CoreDataDrillDown[11323:1403] Save context command fired
2014-08-04 14:54:27.019 CoreDataDrillDown[11323:90b] merged changes to main context
For more than 15 sec UI was irresponsive. I want to know the reason why?
With below approach suggested by https://stackoverflow.com/users/817182/thom-ek and https://stackoverflow.com/users/2128900/micha%c5%82-ciuba, insertion is quick and does not freeze UI but changes are not getting saved to disk, not sure why.
//writerContext has persistent store coordinator, so it should write data to disk but it
//is not writing any changes to disk.???????
-(NSManagedObjectContext *)writerContext
{
if (!_writerContext)
{
_writerContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_writerContext.persistentStoreCoordinator = [(AppDelegate *)[[UIApplication sharedApplication] delegate] persistentStoreCoordinator];
}
return _writerContext;
}
//mainContext is being used with NSFetchedResultController
-(NSManagedObjectContext *)mainContext
{
if (!_mainContext)
{
_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainContext.parentContext = [self writerContext];
}
return _mainContext;
}
//performBlock is being called on privateContext
-(NSManagedObjectContext *)privateContext
{
if (!_privateContext)
{
_privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_privateContext.parentContext = [self mainContext];
}
return _privateContext;
}
Main problem is that mainContext is still tied to mainQueue, so it will wait for this huge merge.
There are good articles about asynchronous saves: on Cocoanetics and ObjC.io.
Other solution is just to reset mainContext and reload all tableViews (you don’t show all of 20k records to user at once).
I'm trying to separate my application work when there is a bigger work to do to optimize performance. My problem is about a NSManagedObjectContext used in another thread than the main one.
I'm calling:
[NSThread detachNewThreadSelector:#selector(test:) toTarget:self withObject:myObject];
On the test method there are some stuff to do and I have a problem here:
NSArray *fetchResults = [moc
executeFetchRequest:request
error:&error];
Here is my test method:
-(void) test:(MyObject *)myObject{
#autoreleasepool {
//Mycode
}
}
The second time I call the test method, my new thread is blocked when the executeFetchRequest is called.
This problem arrived when my test method is called more than one time in succession. I think the problem comes from the moc but I can't really understand why.
Edit:
With #Charlie's method it's almost working. Here is my code to save my NSManagedObjectContext (object created on my new thread).
- (void) saveContext:(NSManagedObjectContext *) moc{
NSError *error = nil;
if ([moc hasChanges] && ![moc save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
This method is called on the new thread. My problem now is that with this save, I have a deadlock and I don't really understand why. Without it's perfectly working.
Edit2
I'm working on this issue but I still can't fix it. I changed my code about the detachNewThreadSelector. Here is my new code:
NSManagedObjectContext* context = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.persistentStoreCoordinator = self.persistentStoreCoordinator;
context.undoManager = nil;
[context performBlock:^
{
CCImages* cachedImage;
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
childContext.parentContext = context;
cachedImage=[CCImages getCCImageForKey:path inManagedObjectContext:childContext];
UIImage *image = [self getImageFromCacheWithPath:path andCachedImage:cachedImage atDate:now];
if (image != nil){
if(![weakSelf.delegate respondsToSelector:#selector(CacheCacheDidLoadImageFromCache:)])
[weakSelf setDelegate:appDelegate.callbacksCollector];
//[weakSelf useCallbackCollectorForDelegate:weakSelf inMethod:#"initPaginatorForListMoments"];
[weakSelf.delegate CacheCacheDidLoadImageFromCache:image];
}
}
- (UIImage*) getImageFromCacheWithPath:(NSString*) path andCachedImage:(CCImages *) cachedImage atDate: (NSDate *) now{
NSURL* localURL=[NSURL URLWithString:cachedImage.path relativeToURL:[self imageCacheDirectory]];
UIImage * image;
//restore uiimage from local file system
if (localURL) {
image=[UIImage imageWithContentsOfFile:[localURL path]];
//update cache
[cachedImage setLastAccessedAt:now];
[self saveContext];
if(image)
return image;
}
return nil;
}
Just after that, I'm saving my contexts (manually for now)
[childContext performBlock:^{
NSError *error = nil;
if (![childContext save:&error]) {
DDLogError(#"Error during context saving when getting image from cache : %#",[error description]);
}
else{
[context performBlock:^{
NSError *error = nil;
if (![context save:&error]) {
DDLogError(#"Error during context saving when getting image from cache : %#",[error description]);
}
}];
}
}];
There is a strange problem. My call back method is called without any problem on my controller (which implements the CacheCacheDidLoadImageFromCache: method). On this method I attest the reception of the image (DDLogInfo) and say that I want my spinner to stop. It does not directly but only 15secondes after the callback method was called.
My main problem is that my context (I guess) is still loading my image from the cache while it was already found. I said 'already' because the callback method has been called and the image was present. There is no suspicious activity of the CPU or of the memory. Instruments didn't find any leak.
I'm pretty sure that I'm using wrongly the NSManagedObjectContext but I can't find where.
You are using the old concurrency model of thread confinement, and violating it's rules (as described in the Core Data Concurrency Guide, which has not been updated yet for queue confinement). Specifically, you are trying to use an NSManagedObjectContext or NSManagedObject between multiple threads.
This is bad.
Thread confinement should not be used for new code, only to maintain the compatibility of old code while it's being migrated to queue confinement. This does not seem to apply to you.
To use queue confinement to solve your problem, first you should create a context attached to your persistent store coordinator. This will serve as the parent for all other contexts:
+ (NSManagedObjectContent *) parentContextWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)coordinator {
NSManagedObjectContext *result = nil;
result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[result setPersistentStoreCoordinator:coordinator];
return result;
}
Next, you want the ability to create child managed object contexts. You will use these to perform work on the data, wether reading or writing. An NSManagedObjectContext is a scratchpad of the work you are doing. You can think of it as a transaction. For example, if you're updating the store from a detail view controller you would create a new child context. Or if you were performing a multi-step import of a large data set, you would create a child for each step.
This will create a new child context from a parent:
+ (NSManagedObjectContext *) childContextWithParent:(NSManagedObjectContext *)parent {
NSManagedObjectContext *result = nil;
result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[result setParent:parent];
return result;
}
Now you have a parent context, and you can create child contexts to perform work. To perform work on a context, you must wrap that work in performBlock: to execute it on the context's queue. I do not recommend using performBlockAndWait:. That is intended only for re-rentrant methods, and does not provide an autorelease pool or processing of user events (user events are what drives nearly all of Core Data, so they're important. performBlockAndWait: is an easy way to introduce bugs).
Instead of performBlockAndWait: for your example above, create a method that takes a block to process the results of your fetch. The fetch, and the block, will run from the context's queue - the threading is done for you by Core Data:
- (void) doThingWithFetchResults:(void (^)(NSArray *results, NSError *error))resultsHandler{
if (resultsHandler != nil){
[[self context] performBlock:^{
NSArray *fetchResults = [[self context] executeFetchRequest:request error:&error];
resultsHandler(fetchResults, error);
}];
}
}
Which you would call like this:
[self doThingsWithFetchResults:^(NSArray *something, NSError *error){
if ([something count] > 0){
// Do stuff with your array of managed objects
} else {
// Handle the error
}
}];
That said, always prefer using an NSFetchedResultsController over using executeFetch:. There seems to be a belief that NSFetchedResultsController is for powering table views or that it can only be used from the main thread or queue. This is not true. A fetched results controller can be used with a private queue context as shown above, it does not require a main queue context. The delegate callbacks the fetched results controller emits will come from whatever queue it's context is using, so UIKit calls need to be made on the main queue inside your delegate method implementations. The one issue with using a fetched results controller this way is that caching does not work due to a bug.
Again, always prefer the higher level NSFetchedResultsController to executeFetch:.
When you save a context using queue confinement you are only saving that context, and the save will push the changes in that context to it's parent. To save to the store you must recursively save all the way. This is easy to do. Save the current context, then call save on the parent as well. Doing this recursively will save all the way to the store - the context that has no parent context.
Example:
- (void) saveContextAllTheWayBaby:(NSManagedObjectContext *)context {
[context performBlock:^{
NSError *error = nil;
if (![context save:&error]){
// Handle the error appropriately.
} else {
[self saveContextAllTheWayBaby:[context parentContext]];
}
}];
}
You do not, and should not, use merge notifications and mergeChangesFromContextDidSaveNotification: with queue confinement. mergeChangesFromContextDidSaveNotification: is a mechanism for the thread confinement model that is replaced by the parent-child context model. Using it can cause a whole slew of problems.
Following the examples above you should be able to abandon thread confinement and all of the issues that come with it. The problems you are seeing with your current implementation are only the tip of the iceberg.
There are a number of Core Data sessions from the past several years of WWDC that may also be of help. The 2012 WWDC Session "Core Data Best Practices" should be of particular interest.
if you want to use managed object context in background thread, there are two approaches,
1 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set the parentContext to main thread context
2 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set persistentStoreCoordinator to main thread persistentStoreCoordinator
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
privateContext.persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
NSManagedObjectContext *moc = mainManagedObjectContext;
if (note.object != moc) {
[moc mergeChangesFromContextDidSaveNotification:note];
}
}];
// do work here
// remember managed object is not thread save, so you need to reload the object in private context
});
before exist the thread, make sure remove the observer, bad thing can happen if you don't
for more details read http://www.objc.io/issue-2/common-background-practices.html
in a core data app with a one-to-many relationship (one "test", many "measures"), I used to have this code :
In AppDelegate.m :
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
In TableViewController.m :
- (NSManagedObjectContext *)managedObjectContext
{
NSManagedObjectContext *context = nil;
id contextDelegate = [[UIApplication sharedApplication] delegate];
if ([contextDelegate performSelector:#selector(managedObjectContext)])
context = [contextDelegate managedObjectContext];
return context;
}
- (void)saveEntryButton:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
if (self.test)
{
// Update existing test
self.test.number = self.numberTextField.text;
}
else // Create new test
{
self.test = [NSEntityDescription insertNewObjectForEntityForName:#"Test" inManagedObjectContext:context];
self.test.number = self.numberTextField.text;
}
if (isSaving)
{
NSManagedObjectContext *context = [test managedObjectContext];
self.measure = [NSEntityDescription insertNewObjectForEntityForName:#"Measure" inManagedObjectContext:context];
[test addWithMeasureObject:measure];
NSData *newDataArray = [NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
self.measure.dataArray = newDataArray;
}
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error])
{
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
}
It works great, but of course, the [NSKeyedArchiver archivedDataWithRootObject:plotDataArray]; can take a few seconds and block the UI so I would like to do it in background.
I spent a few hours to read everything about the concurrency in core data (and I am quite new at it), but I didn't find anything regarding my problem : how to deal with a one-to-many relationship background save ?
What I've tried so far :
In AppDelegate.m
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
//_managedObjectContext = [[NSManagedObjectContext alloc] init];
//[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
In TableViewController.m
- (void)saveEntryButton:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
if (self.test)
{
// Update existing test
self.test.number = self.numberTextField.text;
}
else // Create new test
{
self.test = [NSEntityDescription insertNewObjectForEntityForName:#"Test" inManagedObjectContext:context];
self.test.number = self.numberTextField.text;
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error])
{
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
}
if (isSaving)
{
NSManagedObjectContext *context = [test managedObjectContext];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.parentContext = context;
[temporaryContext performBlock:^{
self.measure = [NSEntityDescription insertNewObjectForEntityForName:#"Measure" inManagedObjectContext:temporaryContext];
[test addWithMeasureObject:measure];
NSData *newDataArray = [NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
self.measure.dataArray = newDataArray;
// push to parent
NSError *error;
if (![temporaryContext save:&error])
{
// handle error
NSLog(#"error");
}
// save parent to disk asynchronously
[context performBlock:^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
NSError *error;
if (![context save:&error])
{
// handle error
NSLog(#"error");
}
}];
}];
}
}
Of course, I receive a SIGABRT error as "test" and "measure" are not in the same context...I've tried a LOT of different things, but I'm really lost.
Thanks in advance for any help.
I see two questions here: background asynchronous saving and what to do with objects in different contexts.
First about saving. Are you sure that it is saving itself that blocks your UI thread and not call to archivedDataWithRootObject? If saving itself is relatively fast, you can consider calling only archivedDataWithRootObject on a background queue, and then communicating the results back to the main queue where you’ll do the save on your UI context.
If it is still the save that takes too long, you can use the approach for background asynchronous saving recommended by Apple. You need two contexts. One context – let’s call it background – is of private queue concurrency type. It is also configured with persistent store coordinator. Another context – let’s call it UI – is of main queue concurrency type. It is configured with background context as a parent.
When working with your user interface, you’re using the UI context. So all managed objects are inserted, modified, and deleted in this context. Then when you need to save you do:
NSError *error;
BOOL saved = [UIContext save:&error];
if (!saved) {
NSLog(#“Error saving UI context: %#“, error);
} else {
NSManagedObjectContext *parent = UIContext.parentContext;
[parent performBlock:^{
NSError *parentError;
BOOL parentSaved = [parent save:&parentError];
if (!parentSaved) {
NSLog(#“Error saving parent: %#“, parentError);
}
}];
}
The save of the UI context is very fast because it doesn’t write data to disk. It just pushes changes to its parent. And because parent is of private queue concurrency type and you do the save inside performBlock’s block, that save happens in background without blocking the main thread.
Now about different managed objects in different contexts from your example. As you discovered, you can’t set an object from one context to a property of an object in another context. You need to choose a context where you need to do the change. Then transfer NSManagedObjectID of one of the objects to the target context. Then create a managed object from ID using one of the context’s methods. And finally set this object to a property of another one.
Essentially you are on the right track, but missing a couple of key elements;
Firstly you will need to transfer test from your main context to the secondary - this is done in the following way;
//this is the object saved in your main managedObjectContext;
NSManagedObjectID *currentTest = test.objectID;
creating the secondary context for adding your related objects can be performed on a background thread. You can use and NSBlockOperation to do the secondary save and create the context at the same time.
here is a simple example using the standard person / address example wired to an IBAction
- (IBAction)button1Click:(id)sender {
NSError *saveError = nil;
// create instance of person to save in our primary context
Person *newParson = [[Person alloc]initIntoManagedObjectContext:self.mainContext];
newParson.name = #"Joe";
[self.mainContext save:&saveError];
//get the objectID of the Person saved in the main context
__block NSManagedObjectID *currentPersonid = newParson.objectID;
//we'll use an NSBlockOperation for the background processing and save
NSBlockOperation *addRelationships = [NSBlockOperation blockOperationWithBlock:^{
// create a second context
NSManagedObjectContext *secondContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[secondContext setPersistentStoreCoordinator:coordinator];
NSError *blockSaveError = nil;
/// find the person record in the second context
Person *differentContextPerson = (Person*)[secondContext objectWithID:currentPersonid];
Address *homeAddress = [[Address alloc]initIntoManagedObjectContext:secondContext];
homeAddress.address = #"2500 1st ave";
homeAddress.city = #"New York";
homeAddress.state = #"NY";
homeAddress.zipcode = #"12345";
Address *workAddress = [[Address alloc]initIntoManagedObjectContext:secondContext];
workAddress.address = #"100 home Ave";
workAddress.city = #"Newark";
homeAddress.state = #"NJ";
homeAddress.zipcode = #"45612";
[differentContextPerson addAddressObject:homeAddress];
[differentContextPerson addAddressObject:workAddress];
[secondContext save:&blockSaveError];
}];
[addRelationships start];
}
in the above initIntoManagedObjectContext is a simple helper method in the NSManagedObject subclass as follows;
- (id)initIntoManagedObjectContext:(NSManagedObjectContext *)context {
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:context];
self = [super initWithEntity:entity insertIntoManagedObjectContext:context];
return self;
}
An important note from Apple docs regarding NSBlockOperation:
You must create the managed context on the thread on which it will be used. If you use NSOperation, note that its init method is invoked on the same thread as the caller. You must not, therefore, create a managed object context for the queue in the queue’s init method, otherwise it is associated with the caller’s thread. Instead, you should create the context in main (for a serial queue) or start (for a concurrent queue).
Overview: I have a refresh process that runs in the background (just using performInBackground is all) and part of that has db updates so I have a separate MOC for the background thread. I then use the didSave notification to merge in the changes, but I'm not see those updates in the DB/my UI. I've logged the object itself before and after the save and I can see the attribute is changed, but in the method called by notification, I log the objects in the context received and it doesnt have updated value. I know some other things are probably ugly, but just trying to figure out this core data piece. I previously had just one MOC and things worked (non core data should be fine), but I've rearchitected things to be in background now and wanted to use guidance of a separate MOC.
Create context, setup notification, set attribute and save. Post save, the value is 0
// this creates context with same PSC as main MOC
NSManagedObjectContext *context = [[MyAppDelegate application] temporaryContext];
[[NSNotificationCenter defaultCenter] addObserver:(MyAppDelegate *)[[UIApplication sharedApplication] delegate]
selector:#selector(managedObjectContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:context];
NSLog(#"value is %d", [[myObject email] boolValue]); //value is 1
[myObject setEmail:[NSNumber numberWithBool:![[myObject email] boolValue]]];
NSError *error;
if (![context save:&error]) {
NSLog(#"Error in saving BriefCase object - error:%#" ,error);
}
NSLog(#"value is %d", [[myObject email] boolValue]); //value is 0 now
Here, I check the MOC I was sent and i see all values are 1. So when merge happens, no updates made. Where is the disconnect if I saw the object had a 0 after saving?
// Called when a ManagedObjectContext performs save operation
- (void)managedObjectContextDidSave:(NSNotification *)notification {
NSManagedObjectContext *receivedMOC = [notification object];
NSArray *items = [MyObjectClass getAllMyObjectsInManagedObjectContext:receivedMOC];
for (int i=0; i < [items count]; i++) {
NSLog(#"value is %d", [[[items objectAtIndex:i] email] boolValue]);
}
add'l context creation code, may be the issue
- (NSManagedObjectContext *)temporaryContext {
NSManagedObjectContext *newContext = [[[NSManagedObjectContext alloc] init] autorelease];
NSPersistentStoreCoordinator *psc = [self.managedObjectContext persistentStoreCoordinator];
[newContext setUndoManager:nil];
[newContext setPersistentStoreCoordinator:psc];
return newContext;
The temporaryContext creates a new MOC on each call, so is seems that you actually work with 2 different background MOCs. myModel is created on a MOC that is different from
NSManagedObjectContext *context = [[MyAppDelegate application] temporaryContext];
and therefore saving the latter has no effect on the changes in myModel.
You have to modify your code such that only one temporary context is created and passed around.
I don't see you calling
[mainMOC mergeChangesFromContextDidSaveNotification:saveNotification];
when handling the notification. This might explain what is happening. This should be called on the main thread.