Multithreaded Core Data
As you know, Core Data is a powerful Apple object graph management framework. There are a lot of articles on Habré about Core Data, nevertheless, multithreading is poorly covered, and, it seems to me, almost everyone asked the question of how to implement it correctly.
In short, the Core Data stack consists of several main parts.
1) NSPersistentStore, which can be a binary file, XML, SQLite file.
2) NSManagedObjectModel, which is a compiled binary version of the data model.
3) NSPersistentStoreCoordinator, is engaged in loading data from NSPersistentStore and NSManagedObjectModel, saving and caching.
4) NSManagedObjectContext, loading data from NSPersistentStore into memory, operation with instances.
5) NSManagedObject - object of the data model.
The main, in my opinion, unpleasant feature of all this miracle is that NSManagedObjectContext is not thread-safe.
With large database sizes, during migrations, the initialization of the stack on the main thread can take more than 30 seconds. This will cause the system to simply kill the application. There is a way out, initialize the stack in another thread.
So, our application was launched, the user did not receive UI lags, everyone is happy. We follow further.
As I wrote above, NSManagedObjectContext is not thread-safe. Therefore, it is appropriate to keep the main application context on the main thread. But in this case, it will slow down the UI while maintaining this context, what should I do? But here, in iOS 6, the NSManagedObjectContext types appeared.
1) NSMainQueueConcurrencyType - available exclusively from the main thread.
2) NSPrivateQueueConcurrencyType - runs on a background thread.
3) NSConfinementConcurrencyType - runs on the thread on which it was created.
And also the opportunity to create child contexts. When saving, the child context pushes all its changes to the parent. Accordingly, it is now possible to equip your manager for working with CoreData as follows.
At this point, the initialization of our manager for working with Core Data ends, now we have a father-context hidden from prying eyes, as well as a daughter on the main thread.
As you can easily guess, when working on background flows, we can create our contexts simply by adding our child context created above as an ancestor.
This context, when saved, will always save the changes to its parent. Thus, we will always have the most relevant information in _defaultManagedObjectContext (which pushes changes to the real parent).
The most important thing left is conservation. Contexts that live on background streams can only be accessed through performBlock: and performBlockAndWait :. Therefore, saving the background stream will look like this.
After saving the child context, you must save the parent.
For several years, I often hear from developers that Core Data has a large number of minuses, so the choice is made in favor of, for example, FMDB , the main argument is multithreading, or rather, supposedly, its absence in Core Data. The purpose of the article is precisely to dispel this myth.
Many frameworks have been written to work with Core Data, the main one, in my opinion, is MagicalRecord . Includes a huge amount of functionality. It is worth noting that inside it works approximately according to the method described above. Any framework needs to be properly applied, which means understanding how it works.
That's all. Thanks for attention.
General Provisions
In short, the Core Data stack consists of several main parts.
1) NSPersistentStore, which can be a binary file, XML, SQLite file.
2) NSManagedObjectModel, which is a compiled binary version of the data model.
3) NSPersistentStoreCoordinator, is engaged in loading data from NSPersistentStore and NSManagedObjectModel, saving and caching.
4) NSManagedObjectContext, loading data from NSPersistentStore into memory, operation with instances.
5) NSManagedObject - object of the data model.
The main, in my opinion, unpleasant feature of all this miracle is that NSManagedObjectContext is not thread-safe.
Stack Initialization
With large database sizes, during migrations, the initialization of the stack on the main thread can take more than 30 seconds. This will cause the system to simply kill the application. There is a way out, initialize the stack in another thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Создание NSManagedObjectModel
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:kModelFileName withExtension:@"momd"];
NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
// Создаем NSPersistentStoreCoordinator
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
// Добавляем к NSPersistentStoreCoordinator хранилище, именно на этой операции приложение может висеть очень долго
NSURL *doсURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject];
NSURL *storeURL = [doсURL URLByAppendingPathComponent:@"CoreData.sqlite"];
NSError *error = nil;
NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error];
// Создание контекстов
});
So, our application was launched, the user did not receive UI lags, everyone is happy. We follow further.
Create main contexts
As I wrote above, NSManagedObjectContext is not thread-safe. Therefore, it is appropriate to keep the main application context on the main thread. But in this case, it will slow down the UI while maintaining this context, what should I do? But here, in iOS 6, the NSManagedObjectContext types appeared.
1) NSMainQueueConcurrencyType - available exclusively from the main thread.
2) NSPrivateQueueConcurrencyType - runs on a background thread.
3) NSConfinementConcurrencyType - runs on the thread on which it was created.
And also the opportunity to create child contexts. When saving, the child context pushes all its changes to the parent. Accordingly, it is now possible to equip your manager for working with CoreData as follows.
// Этот код нужно вызывать после инициализации стека в том же потоке.
// _daddyManagedObjectContext является настоящим отцом всех дочерних контекстов юзер кода, он приватен.
_daddyManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_daddyManagedObjectContext setPersistentStoreCoordinator:psc];
// Далее в главном потоке инициализируем main-thread context, он будет доступен пользователям
dispatch_async(dispatch_get_main_queue(), ^{
_defaultManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
// Добавляем наш приватный контекст отцом, чтобы дочка смогла пушить все изменения
[_defaultManagedObjectContext setParentContext:_daddyManagedObjectContext];
});
At this point, the initialization of our manager for working with Core Data ends, now we have a father-context hidden from prying eyes, as well as a daughter on the main thread.
Creating child contexts
As you can easily guess, when working on background flows, we can create our contexts simply by adding our child context created above as an ancestor.
- (NSManagedObjectContext *)getContextForBGTask {
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:_defaultManagedObjectContext];
return context;
}
This context, when saved, will always save the changes to its parent. Thus, we will always have the most relevant information in _defaultManagedObjectContext (which pushes changes to the real parent).
Saving Contexts
The most important thing left is conservation. Contexts that live on background streams can only be accessed through performBlock: and performBlockAndWait :. Therefore, saving the background stream will look like this.
- (void)saveContextForBGTask:(NSManagedObjectContext *)bgTaskContext {
if (bgTaskContext.hasChanges) {
[bgTaskContext performBlockAndWait:^{
NSError *error = nil;
[backgroundTaskContext save:&error];
}];
// Save default context
}
}
After saving the child context, you must save the parent.
- (void)saveDefaultContext:(BOOL)wait {
if (_defaultManagedObjectContext.hasChanges) {
[_defaultManagedObjectContext performBlockAndWait:^{
NSError *error = nil;
[_defaultManagedObjectContext save:&error];
}];
}
// А после сохранения _defaultManagedObjectContext необходимо сохранить его родителя, то есть _daddyManagedObjectContext
void (^saveDaddyContext) (void) = ^{
NSError *error = nil;
[_daddyManagedObjectContext save:&error];
};
if ([_daddyManagedObjectContext hasChanges]) {
if (wait)
[_daddyManagedObjectContext performBlockAndWait:saveDaddyContext];
else
[_daddyManagedObjectContext performBlock:saveDaddyContext];
}
}
Conclusion
For several years, I often hear from developers that Core Data has a large number of minuses, so the choice is made in favor of, for example, FMDB , the main argument is multithreading, or rather, supposedly, its absence in Core Data. The purpose of the article is precisely to dispel this myth.
Many frameworks have been written to work with Core Data, the main one, in my opinion, is MagicalRecord . Includes a huge amount of functionality. It is worth noting that inside it works approximately according to the method described above. Any framework needs to be properly applied, which means understanding how it works.
That's all. Thanks for attention.