Reputation: 116
I use coredata in my app, with 3 contexts:
__masterManagedObjectContext -> is the context that has the NSPersistentStoreCoordinator and save the data to disk.
_mainManagedObjectContext -> is the context used by app, everywhere
dispatchContext -> context used in background method, where i have my webservice access and all coredata insertion/update stuff.
I'll put some code to realize my solution:
App initialisation code:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions //a app começa aqui
{
NSPersistentStoreCoordinator *coordinator = [self newPersistentStoreCoordinator];
__masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[__masterManagedObjectContext setPersistentStoreCoordinator:coordinator];
_mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainManagedObjectContext setUndoManager:nil];
[_mainManagedObjectContext setParentContext:__masterManagedObjectContext];
return YES;
}
Method to create a new store cordinator
- (NSPersistentStoreCoordinator *)newPersistentStoreCoordinator
{
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"example.sqlite"];
NSError *error = nil;
NSPersistentStoreCoordinator *pC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self newManagedObjectModel]];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
if (![pC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])
{
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return pC;
}
- (NSManagedObjectModel *)newManagedObjectModel
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"example" withExtension:@"momd"];
NSManagedObjectModel *newManagedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return newManagedObjectModel;
}
Thread call with context specification (essential code):
@try
{
dispatchContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[dispatchContext setUndoManager:nil];
[dispatchContext setParentContext:__masterManagedObjectContext];
NSNotificationCenter *notify = [NSNotificationCenter defaultCenter];
[notify addObserver:self selector:@selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:dispatchContext];
if(dispatchContext != nil)
{
[NSThread detachNewThreadSelector:@selector(parseDataWithObjects) toTarget:self withObject:nil];
}
else
{
NSLog(@"context IS NIL");
}
}
The background method:
- (void)parseDataWithObjects
{
[dispatchContext lock];
...
webservice data parse, and core data inserting/updating (+/- 5MB)
...
[dispatchContext save:&error];
[dispatchContext unlock];
[__masterManagedObjectContext save:nil];
}
This method is caled in all UI to access coredata data.
- (NSManagedObjectContext *)managedObjectContext
{
return _mainManagedObjectContext;
}
A calling example:
NSManagedObjectContext *context = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
...fetching, update, ...
Now, my really problem:
Whenever the master context is to be saved ([__masterManagedObjectContext save:nil];
, on background), when I try to access the main context (_mainManagedObjectContext
), the app freezes (maybe a lock?).
The saving process takes a long time (because are a lots of data (aprox. 6mb)). While saving, the app turns slow and if i access some data while this process is running, my app freezes forever (i need to force quit).
Another problem is to merge the contexts. Imagine, using the main context in other viewController, and saving that context, everything works fine until i close de app. When i open the app again, nothing was saved.
What i'm doing wrong? Until now, this contexts are confusing to me. Someone can help me? I really appreciate :)
---------- EDIT:
Following Florian Kugler response, now i have only 2 context, every one with the same coordinator.
When my app is initialised, i call this method:
-(void) createContexts
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"example" withExtension:@"momd"];
NSManagedObjectModel *newManagedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"example.sqlite"];
NSError *error = nil;
pC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:newManagedObjectModel];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
if (![pC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])
{
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
mainManagedObjectContext.persistentStoreCoordinator = pC;
}
- (void)mergeChanges:(NSNotification*)notification {
[mainManagedObjectContext performBlock:^{
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}];
}
- (void)saveMasterContext
{
[mainManagedObjectContext performBlock:^{
[mainManagedObjectContext save:nil];
}];
}
To start my data import (on background), i use this code:
NSNotificationCenter *notify = [NSNotificationCenter defaultCenter];
[notify addObserver:self selector:@selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundContext];
[NSThread detachNewThreadSelector:@selector(parseDataWithObjects) toTarget:self withObject:nil];
My background method:
- (void)parseDataWithObjects
{
[self resetTime];
backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
backgroundContext.persistentStoreCoordinator = pC;
...
[backgroundContext save:&error];
}
Resuming...
And the performance is really better. But the app freezes a little, i think is on "mergeChanges". I'm doing something wrong?
Upvotes: 3
Views: 2338
Reputation: 688
When using the NSPrivateQueueConcurrencyType
or NSMainQueueConcurrencyType
you should wrap everything you do on these contexts in performBlock:
. This makes sure, that these commands get executed on the right queue. For example when you save the master context:
[__masterManagedObjectContext performBlock:^{
[__masterManagedObjectContext save];
}];
Furthermore you don't have to merge the changes manually from the dispatch context if you set it up as a child to your master context. As soon as you save the child context the changes will be pushed into the parent context.
Another problem is that you are initializing a context with the NSConfinementConcurrencyType on one thread and then use it on a different thread. With this concurrency type it is very important that you initialize the context on the thread you are going to use it.
However, I would suggest you don't use NSConfinementConcurrencyType at all. One possible alternative would be to setup your dispatch context with NSPrivateQueueConcurrencyType
:
NSManagedObjectContext* dispatchContext = [[NSManagedObjectContext] alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
dispatchContext.parentContext = __masterManagedObjectContext;
[dispatchContext performBlock:^{
[self parseDataWithObjects];
}];
When you do this there is no need to acquire locks, everything you do within the block will be processed on a private serial queue.
You already mentioned that saving takes quite long and that your app becomes unresponsive.
Your managed object context setup with three nested contexts (private <- main <- dispatch) is not the best choice if you plan to import larger amounts of data in the dispatch context. This will always block the main thread for significant amounts of time, because all the changes you make in the dispatch context have to be copied into the main context before they can be saved in the "root" context.
I recently wrote an article about this, comparing the performance of different core data setups. In a follow up post I explain in greater detail why this setup takes so much time on the main thread.
For importing large amounts of data it is MUCH faster to use independent managed object contexts with a common persistent store coordinator. You create one context with NSMainQueueConcurrencyType
(which you use for all UI related stuff), and another one with NSPrivateQueueConcurrencyType
(for importing data). You assign the same persistent store coordinator to both of them.
// assuming you have persistendStoreCoordinator
NSManagedObjectContext* mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
mainContext.persistentStoreCoordinator = persistentStoreCoordinator;
NSManagedObjectContext* backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
backgroundContext.persistentStoreCoordinator = persistentStoreCoordinator;
This setup doesn't provide the automatic change propagation you get with nested contexts, but this is very easy to do via the save notification. Notice the use of performBlock:
again, so that the merging happens on the right thread:
// ...
NSNotificationCenter *notify = [NSNotificationCenter defaultCenter];
[notify addObserver:self selector:@selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundContext];
// ...
- (void)mergeChanges:(NSNotification*)notification {
[mainContext performBlock:^{
[mainContext mergeChangesFromContextDidSaveNotification:notification];
}];
}
Hope this helps!
Upvotes: 8