Pull to refresh

Мультиконтекстность в Core Data

Reading time 10 min
Views 23K
Original author: Oliver Drobnik
Всем привет.

Когда Вы начинаете использовать CoreData для сохранения данных в Ваших приложениях, Вы начинаете работать с единственным управляемым объективным контекстом / managed object context (MOC). Это — то, что используется в шаблоне при создание проекта в xCode, если Вы при создание проекта ставите галочку рядом с «Use Core Data».

image



Использование CoreData в сочетании с NSFetchedResultsController значительно упрощает работу с любым видом списка элементов, которые выводятся на экране в табличном представлении.

Есть два сценария, в которых Вы хотели бы разветвляться, т.е. используя несколько управляемых объективных контекстов: 1) чтобы упростить процесс добавления / редактирования новых элементов и 2) избежать блокировать UI. В этом посте я хочу рассмотреть пути создания Ваших контекстов, чтобы получить, то, что Вы хотите.

Во-первых, давайте рассмотрим установку единственного контекста. Вам нужен persistent store coordinator (координатор постоянного хранилища) (PSC), чтобы обращаться к файлу базы данных на диске. Таким образом, чтобы данный координатор понимал, как структурирована данная база данных, Вам нужна модель. Эта модель объединена из всех определений модели, содержавшихся в проекте, и указывает CoreData об этой структуре БД. Координатор устанавливается на управляемый объект контекста через свойство функций. Запомните первое правило: управляемый объективный контекста с помощью координатора запишется на диск, если Вы вызовите saveContext.

image

Рассмотрим эту схему. Каждый раз, когда Вы вставляете, обновляете или удаляете сущность в этом единственном управляемом объективном контексте, то контроллер выбранных результатов будет уведомлен об этих изменениях и обновит свое содержание табличного представления. Это не зависит от сохранения контекста. Вы можете сохранять так редко или часто, как Вы хотите. Шаблон Apple экономит на каждом добавлении сущности и также (как не странно) на applicationWillTerminate.

Этот подход в основном подходит для большинства основных случаев, но как я говорил раньше, есть две проблемы с ним. Первая связана с добавлением новой сущности. Вы, вероятно, хотите снова использовать то же визуальное представление ьбю/ для добавления и редактирования сущности. Таким образом, вы, возможно, захотите создать новую сущность даже прежде, чем заполните визуализацию представления для него. Это заставило бы уведомления об обновлении инициировать обновление на контроллере выбранных результатов, т.е. пустая строка появится незадолго до того, как концепция MVC полностью появиться для добавления или редактирования.

Вторая проблема была бы очевидна, если бы обновления накапливались, прежде чем saveContext станет слишком обширным, и операция сохранения заняла бы больше времени, чем 1/60-й секунды. Поскольку в этом случае пользовательский интерфейс был бы заблокирован, пока сохранение не завершиться, и у Вас был бы значимый переход, например, при прокрутке.

Обе проблемы могут быть решены, используя несколько управляемых объективных контекстов.

«Традиционный» мультиконтекстный подход

Подумайте о каждом управляемом объективном контекста, как о временном блокноте изменений. Перед выходом iOS 5 Вы наверно слышали о изменениях в других контекстах и объединили изменения с момента уведомления в основной контекст. Типичная установка была бы похожа на эту блок-схему:

image

Создайте временный контекст для его использования для очереди фоновых задач. И сохраните там изменения, установите тот же координатор постоянного хранилища на временном контексте, как и в основном контексте. По мнению Маркуса Сарра это должно выглядеть так:

Несмотря на то, что NSPersistentStoreCoordinator не ориентирован на многопотоковое исполнение, NSManagedObjectContext знает, как заблокировать его должным образом, когда тот находиться в использовании. Поэтому, мы можем присоединить столько NSManagedObjectContext объектов к NSPersistentStoreCoordinator, сколько мы хотим, не опасаясь коллизии.

Вызов saveContext на фоновом контексте запишет изменения в файл хранилища и также инициирует NSManagedObjectContextDidSaveNotification.

В коде это будет примерно выглядеть следующим образом:

dispatch_async(_backgroundQueue, ^{
   // create context for background
   NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] init];
   tmpContext.persistentStoreCoordinator = _persistentStoreCoordinator;
 
   // something that takes long
 
   NSError *error;
   if (![tmpContext save:&error])
   {
      // handle error
   }
});


Создание временного контекста происходит очень быстро, таким образом, Вы не должны волновать по поводу частого создания и выпуска этих временный контекстов. Дело в том, что чтобы установить persistentStoreCoordinator на тот же самый главный контекст так, создание также должно произойти в фоновом режиме.

Я предпочитаю эту упрощенную установку стека CoreData:

- (void)_setupCoreDataStack
{
   // setup managed object model
   NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Database" withExtension:@"momd"];
   _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
 
   // setup persistent store coordinator
   NSURL *storeURL = [NSURL fileURLWithPath:[[NSString cachesPath] stringByAppendingPathComponent:@"Database.db"]];
 
   NSError *error = nil;
   _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_managedObjectModel];
 
   if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) 
   {
   	// handle error
   }
 
   // create MOC
   _managedObjectContext = [[NSManagedObjectContext alloc] init];
   [_managedObjectContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
 
   // subscribe to change notifications
   [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_mocDidSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:nil];
}


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

- (void)_mocDidSaveNotification:(NSNotification *)notification
{
   NSManagedObjectContext *savedContext = [notification object];
 
   // ignore change notifications for the main MOC
   if (_managedObjectContext == savedContext)
   {
      return;
   }
 
   if (_managedObjectContext.persistentStoreCoordinator != savedContext.persistentStoreCoordinator)
   {
      // that's another database
      return;
   }
 
   dispatch_sync(dispatch_get_main_queue(), ^{
      [_managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
   });
}


Во-первых, мы хотим избежать слияния наших собственных изменений. Также, если у нас есть несколько БД CoreData в том же приложении мы пытается избежать слияния изменений, которые предназначены для другой БД. Я сталкивался с такой проблемой в одном из моих приложений, поэтому я и проверяю координатор постоянного хранилища. Наконец осуществите слияние изменений с помощью метода mergeChangesFromContextDidSaveNotification. У уведомления есть словарь всех изменений в его полезной нагрузке, и этот метод знает об интеграции их в контекст.

Передача управляемых объектов между контекстами

Категорически запрещается перемещать управляемый объект, который вы получили из одного контекста в другой. Существует простой способ разобраться с «зеркалом» управляемого объекта через ObjectID. Этот идентификатор ориентирован на многопотоковое исполнение, и Вы можете всегда получить его от одного экземпляра NSManagedObject и затем вызвать objectWithID. Второй контекст тогда получит свою собственную копию управляемых объектов для работы с ним.

NSManagedObjectID *userID = user.objectID;
 
// make a temporary MOC
dispatch_async(_backgroundQueue, ^{
   // create context for background
   NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] init];
   tmpContext.persistentStoreCoordinator = _persistentStoreCoordinator;
 
   // user for background
   TwitterUser *localUser = [tmpContext objectWithID:userID];
 
   // background work
});


Описанный подход является полностью обратно совместимым вплоть до первой версии IOS, которая получила поддержку CoreData с IOS 3. Если Вам нужна поддержка только IOS 5 для вашего приложения, то существует более современный подход, который мы рассмотрим ниже.

Родительский/дочерний контекст

В IOS 5 появилась возможность для управляемого объектного контекста содержать parentContext. Вызов метода saveContext пушит изменения из дочернего контекста к родителю без необходимости прибегать к способу, который включает слияния содержимого из словаря, описывающего изменения. В то же время Apple, добавили возможность для контекстов иметь их собственное отдельную очередь для выполнения изменения как синхронно так и асинхронно.

Тип параллелизма очереди, задается в новом инициализаторе initWithConcurrencyType на NSManagedObjectContext. Обратите внимание на то, что в этой схеме я добавил несколько дочерних контекстов, так что у всех есть та же основная очередь контекста, как и у родителя.

image

Дочерний контекст каждый раз, при сохранении будет сохранять изменения в своего родителя, и это приводит к тому, что контроллер выбранных результатов должен также знать об этих изменениях. Однако это еще не сохраняет данные, так как фоновый контекст не знает о координаторе постоянного хранилища. Для получения данных на диск нужен дополнительный метод saveContext: на главной очереди контекста.

Первое необходимое изменение для этого подхода заключается в изменении основного контекста типа параллелизма на NSMainQueueConcurrencyType. В вышеупомянутом _setupCoreDataStack изменения начальной строки выглядят, как показано ниже и нет больше необходимости получать уведомления о слиянии.

_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:_persistentStoreCoordinator];


Длительная фоновая операция будет выглядеть следующим образом:

NSMangedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.parentContext = mainMOC;
 
[temporaryContext performBlock:^{
   // do something that takes some time asynchronously using the temp context
 
   // push to parent
   NSError *error;
   if (![temporaryContext save:&error])
   {
      // handle error
   }
 
   // save parent to disk asynchronously
   [mainMOC performBlock:^{
      NSError *error;
      if (![mainMOC save:&error])
      {
         // handle error
      }
   }];
}];


Каждому контексту теперь необходимо использовать performBlock: (async) или performBlockAndWait: (sync) для работы. Это гарантирует, что операции, содержавшиеся в блоке, используют корректную очередь. В вышеупомянутом примере длинная операция выполняется на фоновой очереди. Как только у вас все будет готово, и изменения перенаправляться к родителю через метод saveContext тогда появиться асинхронный метод performBlock для сохранения mainMOC. И будет снова происходить на корректной очереди, как предусмотрено performBlock.

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

Удивительное упрощение, предоставленное этим подходом, состоит в том, что Вы можете создать временный контекст (как дочерний элемент) для любой визуализации представления, у которого есть кнопка Save и Cancel. Если Вы передаете управляемый объект для редактирования, то Вы переносите его (через objectID, упомянутый выше) к временному контексту.Пользователь имеет возможность обновлять все элементы управляемого объекта. Если он нажимает на Save то сохраняется весь временный контекст. Если он нажимает на cancel, то ничего не нужно делать, потому что изменения отбрасываются вместе с временным контекстом.

У вас еще не крутится голова от всей этой информации? Если нет, то вот высший пилотаж о многоконтекстности CoreData.

Асинхронное сохранение данных

Гуру Core Data Marcus Zarra показал мне следующий подход, который основывается на вышеупомянутом Parent/Child методе, но добавляет дополнительный контекст исключительно для записи на диск. Как упоминалось ранее долгую операцию записи, мог бы блокировать основной поток в течение короткого времени, заморозив UI. В рамках этого разумного подхода запись выделяется в отдельную очередь, и пользовательский интерфейс сохраняет плавность работы (остаётся плавным, не «зависает»).

image

Настройка для CoreData также довольно проста. Нужно только переместить persistentStoreCoordinator в наш новый скрытый контекст и сделать основной контекст дочерним элементом.

// create writer MOC
_privateWriterContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateWriterContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
 
// create main thread MOC
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedObjectContext.parentContext = _privateWriterContext;


Теперь нужно осуществить три разных сохранения для каждого обновления: временного контекста, главного контекста UI и для записи на диск. Но так же легко, как и раньше, можно реализовать стек performBlocks… Пользовательский интерфейс остается разблокирован в течение длительной операции базы данных (например, импорт большого количества записей), а также, когда это записано на диск.

Заключение

iOS 5 значительно упростил работу с CoreData на фоновых очередях, и получил изменения, исходящих от дочерних контекстов к их родителям. Если Вы все еще используете iOS 3/4 тогда, это все функции не доступны для Вас. Но если Вы начинаете новый проект, у которого есть iOS 5 как минимальное требование, Вы можете сразу создать Турбо Подход Маркуса Сарры, как описано выше.

Зак Волдовский указал мне, что использование скрытого типа параллелизма очереди для “редактирования визуализации представления ” может быть излишним. Если используете NSContainmentConcurrencyType вместо визуализации представления дочернего контекста тогда, Вам не нужно обертка performBlock. Все что нужно, так это performBlock на mainMOC для сохранения.

Тип параллелизма ограничения – это “старый способ” выполнения контекстов, но это не означает, что он был традиционным. Он просто привязывает операции контекста к самоуправляемой модели потоков. Набор оборотов скрытой очереди для каждого нового контроллера является расточительным, ненужным, и медленным.-performBlock: и-performBlockAndWait: не работайте с типом параллелизма ограничения по той причине, что ни блоки, ни блокировка не необходимы, когда Вы создаете несколько контекстов в способе.

NSManagedObjectContext знает, как сохранить и объединиться разумно, и поэтому основной контекст потока привязан к основному потоку, его слияния всегда выполняются безопасно. Редактирование визуализации представления связано с основным потоком точно так же, как основная визуализация представления; единственным способом является — отдельная операция, которая находится только в UI, поэтому он подходит для использования типа параллелизма ограничения здесь. Контекст редактирования концептуально не “новая” вещь, он просто откладывает изменение на позже, все еще позволяя Вам отменить изменения полностью.

Таким образом, это действительно сводится к Вашему персональному предпочтению: скрытая очередь с performBlock или без параллелизма ограничения. Что касается меня, то я стараюсь предпочитать скрытые очереди из-за безопасности, что я получаю от их использования.

p.s. Для многих может показаться что статья бесполезная, но надеюсь что некоторые все же вынесут что-то полезного из этой статьи. Не ругайте сильно за перевод, если есть замечания, пишите в личку, исправим :)
Tags:
Hubs:
+4
Comments 1
Comments Comments 1

Articles