CoreData多上下文的设计

将CoreData和NSFetchedResultsController结合使用,可以简化任意类型表视图的处理

有两个场景,你可能想要使用多个上下文来处理:
1、简化添加、编辑新的条目
2、避免阻塞UI
在这篇文章中,我想回顾一下设置上下文的方法,以便为您提供所需的内容。

首先,让我们回顾下Context的设计。我们需要一个持久化协调器(persistentStoreCoordinator)来管理与数据库文件的通讯。因此你需要一个model,让PSC知道数据库的结构。这个model将合并工程中定义的所有model,让CoreData知道关于数据库的结构信息。PSC设置在MOC的一个属性上。记住第一条规则:设置过PSC属性的MOC调用saveContext的时候会将数据写到磁盘。

image.png

观察上图。无论何时你在这个单一的MOC上插入、更新或者删除一个实体,fetchedResultsController都会收到一个改变和更新表视图内容的通知。这与上下文的保存无关。您可以根据需要尽可能少地保存。Apple的模板在每次添加实体时保存,并且在applicationWillTerminate中保存。

这种方法适用于大部分情况,但正如我上面提到的,它有两个问题。您可能希望重用相同的视图控制器来添加和编辑实体。因此,您可能希望在呈现VC之前创建一个新实体,以便填充。这将导致更新通知触发对fetchedResultsController的更新,即在呈现用于添加或编辑的模态视图控制器之前,会短暂的出现空行。

如果在saveContext之前产生的更新太大,而且保存时间超过1/60秒,那么第二个问题会很明显。因为这种情况下,用户界面将被阻塞,直到保存完成,并且在滚动时有明显的跳转。

这两个问题都可以通过使用多上下文来解决。

传统的多上下文方式

将每个上下文视为一个临时的暂存器。iOS5之前,你能监听其他上下文的改变,并且通过通知在主线上下文合并其他上下文的变更。一个典型的设计如下图:

image.png

你将创建一个临时上下文,在后台队列使用。为临时上下文设置和主线程上下文一样的PSC,临时下文也可以保存变更到持久化文件。Marcus Zarra如是说:

尽管NSPersistentStoreCoordinator也不是线程安全的,但是NSPersistentStoreCoordinator知道在使用的时候如何正确锁定它。因此,我们可以根据需要将多个MOC加到单个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
   }
});

创建一个临时的上下文是非常快的,你不必担心频繁的创建和删除那些临时下文。关键点是设置临时上下文的PSC与主线程上下文的PSC相同,以便写入也可以在后台进行。

我更喜欢简单的设置CoreData stack:

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

现在请思考收到did save通知时,如何处理通知:

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

第一个if,忽略自身的改变。假如我们的APP有多个CoreData DB,我们要阻止合并其他DB的改变。为什么我要检查PSC,因为在我的app中碰到过这样的问题。最后,我们通过系统提供的mergeChangesFromContextDidSaveNotification方法,合并改变。通知有一个包含所有更改的字典,并且这个方法知道如何将它们插入MOC。

上下文之间传递托管对象

禁止在上下文中传递托管对象。有一个简单的方法检索托管对象,使用它的ObjectID。ObjectID是线程安全的,你总是能在NSManagedObject的实例获取到,然后在另一个上下文通过ObjectID获取托管对象的副本。

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
});

CoreData在iOS3第一个版本被引入后,你可以一直使用上面的方法。如果你的应用程序最低支持iOS5,那么可以使用一种更加先进的方法。

Parent/Child Contexts

iOS5可以让MOC拥有一个parentContext。当childContext调用saveContext的时候,parentContext不需要通过通知合并子上下文的变更。同时苹果为MOC增加了在专有队列进行同步或异步执行改变的功能。

队列的并发类型在NSManagedObjectContext初始化的时候通过initWithConcurrencyType指定。注意下图,我添加了多个子上下文,他们拥有同一个主队列父上文。

image.png

当子上下文保存时,父上文就会获取这些变化,fetchResultController也会得到这些变化。由于后台子上下文不知道PSC,所以这些变化不会保存到持久化文件。要保存这些变化到持久化文件,你需要在主线程父上下文调用saveContext。

首先需要改变主MOC的并发类型为NSMainQueueConcurrencyType。上面提到的_setupCoreDataStack中MOC的初始化行改变如下,并且不再需要合并通知。

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

现在,每个MOC都需要与performBlock: (async)或performBlockAndWait: (sync)一起使用。确保block中的操作在正确的线程执行。在上面的例子中,操作在后台队列执行。操作完成后,临时上下文通过save方法将改变推送到父上下文,然后mainMOC也有一个performBlock异步保存。再次使用performBlock,保证操作在正确的队列上执行。

子上下文不会自动获取父上下文的更新。你可以重新加载它们以获得更新,大部分情况下它们是临时的,因此我们不需要更新它们。只要主队列的MOC获得更新,fetchResultController也获得更新,我们就可以保存主MOC。

这种方式带来的简化是,你可以在点击取消和保存按钮的时候,为任意视图控制器创建一个临时上下文(作为子上下文)。编辑的时候,可通过objectID将托管对象传递到临时上下文。用户可以更新托管对象的所有属性,如果他点击Save,则保存临时上下文。如果他点击取消,你不必做任何事情,因为变更将和临时上下文一起丢弃。

你晕了吗?如果没有,这就多上下文设计的全部。

异步保存

CoreData专家Marcus Zarra向我们展示了以下方法,该方法基于上面的父/子方法新增了一个专门用于写入磁盘的私有上下文。如前所述,长时间的写操作可能阻塞主线程UI。这种聪明的做法将写操作放在私有队列执行,使UI保持平滑。

image.png

CoreData的设置也非常简单,我们只需要将persistentStoreCoordinator移动到我们专有的用来执行写操作的MOC,并设置主MOC为他的child。

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

现在,每次更新,我们需要执行3次save操作:临时上下文,主上下文(UI上下文)、私有写上下文。但是通过嵌套performBlocks方法,实现也是非常简单的。在长时间的数据库操作期间(导入大量数据)以及写入磁盘的时候,用户界面保持畅通无阻。

结论

iOS极大地简化了后台队列处理CoreData和父上下文获取子上下文的变更。如果你的项目仍然需要支持iOS3/4,那这些依然无法满足你的需求。如果你正在开始一个最低支持iOS5及以上的项目,你可以立即使用Marcus Zarra的方法设计它,如上所述。

说明

水平有限,按自己的理解写的,权当学习笔记,欢迎斧正,英文好的同学请查看原文

相关链接:

https://www.cocoanetics.com/2013/02/zarra-on-locking/
https://code.tutsplus.com/tutorials/core-data-from-scratch-concurrency--cms-22131
http://www.cimgf.com/2011/05/04/core-data-and-threads-without-the-headache/

你可能感兴趣的:(CoreData多上下文的设计)