Pull to refresh

Многопоточная Core Data

Reading time4 min
Views25K
Как известно, Core Data является мощным Apple фреймворком для управления объектным графом. На Хабре немало статей о Core Data, тем не менее, многопоточность освещена достаточно слабо, а, как мне кажется, вопросом о том как правильно ее реализовать, задавался почти каждый.

Общие положения


Если вкратце, то стек Core Data состоит из нескольких основных частей.



1) NSPersistentStore, которым может выступать бинарный файл, XML, SQLite файл.
2) NSManagedObjectModel, которая является скомпилированной бинарной версией модели данных.
3) NSPersistentStoreCoordinator, занимается загрузкой данных из NSPersistentStore и NSManagedObjectModel, сохранением и кешированием.
4) NSManagedObjectContext, загрузка данных из NSPersistentStore в память, операции с экземплярами.
5) NSManagedObject — объект модели данных.

Основной, на мой взгляд, неприятной особенностью всего этого чуда является то, что NSManagedObjectContext не thread-safe.

Инициализация стека


При больших размерах БД, при миграциях инициализация стека на главном потоке может занимать более 30 секунд. Это приведет к тому, что система просто убьет приложение. Выход есть, инициализировать стек в другом потоке.

    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];
       
       // Создание контекстов

});


Итак, наше приложение было запущено, юзер не получил лагов UI, все довольны. Следуем далее.

Создание главных контекстов


Как я писал выше, NSManagedObjectContext не thread-safe. Следовательно главный контекст приложения уместно держать именно на главном потоке. Но в таком случае будет тормозить UI при сохранении этого самого контекста, что же делать? А вот что, в iOS 6 появились типы NSManagedObjectContext.

1) NSMainQueueConcurrencyType – доступен исключительно с главного потока.
2) NSPrivateQueueConcurrencyType – работает на фоновом потоке.
3) NSConfinementConcurrencyType – работает на том потоке, на котором создали.

А также появилась возможность создавать дочерние контексты. Дочерний контекст при сохранении пушит все свои изменения родителю. Соответственно, теперь появилась возможность обустроить свой менеджер по работе с CoreData следующим образом.

// Этот код нужно вызывать после инициализации стека в том же потоке. 
// _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];
    });


На этом месте инициализация нашего менеджера по работе с Core Data заканчивается, теперь у нас есть скрытый от любопытных глаз отец-контекст, а также дочка на главном потоке.

Создание дочерних контекстов


Как можно легко догадаться, при работе на бекграунд-потоках мы можем создавать свои контексты, просто добавляя наш, созданный выше, дочерний контекст в качестве предка.

- (NSManagedObjectContext *)getContextForBGTask {
    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [context setParentContext:_defaultManagedObjectContext];
    return context;
}


Такой контекст при сохранении всегда будет сохранять изменения в своего родителя. Таким образом у нас всегда будет самая актуальная информация в _defaultManagedObjectContext (который пушит изменения в реального родителя).

Сохранение контекстов


Осталось самое главное — сохранение. К контекстам, которые живут на бекграунд-потоках обращаться можно только через performBlock: и performBlockAndWait:. Поэтому сохранение бекграунд потока будет выглядеть следующим образом.

- (void)saveContextForBGTask:(NSManagedObjectContext *)bgTaskContext {
    if (bgTaskContext.hasChanges) {
        [bgTaskContext performBlockAndWait:^{
            NSError *error = nil;
            [backgroundTaskContext save:&error];
        }];
       // Save default context
    }
}


После сохранения дочернего контекста необходимо сохранить родительский.

- (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];
    }
}


Заключение


На протяжении нескольких лет я часто слышу от разработчиков, что Core Data имеет большое количество минусов, поэтому выбор делается в пользу, например, FMDB, основным аргументом является многопоточность, вернее, якобы, ее отсутствие в Core Data. Целью стати является именно развеивание этого мифа.
Для работы с Core Data написано немало фреймворков, основным, на мой взгляд является MagicalRecord. Включает в себя огромное количество функционала. Стоит отметить, что внутри работает примерно по способу, описанному выше. Любой фреймворк нужно грамотно применять, а значит понимать как он работает.

На этом все. Спасибо за внимание.
Tags:
Hubs:
+6
Comments13

Articles