美文网首页iOS数据缓存
CoreData多上下文的设计

CoreData多上下文的设计

作者: kawa007 | 来源:发表于2018-08-03 16:22 被阅读0次

    将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多上下文的设计

        本文链接:https://www.haomeiwen.com/subject/eequvftx.html