将CoreData和NSFetchedResultsController结合使用,可以简化任意类型表视图的处理
有两个场景,你可能想要使用多个上下文来处理:
1、简化添加、编辑新的条目
2、避免阻塞UI
在这篇文章中,我想回顾一下设置上下文的方法,以便为您提供所需的内容。
首先,让我们回顾下Context的设计。我们需要一个持久化协调器(persistentStoreCoordinator)来管理与数据库文件的通讯。因此你需要一个model,让PSC知道数据库的结构。这个model将合并工程中定义的所有model,让CoreData知道关于数据库的结构信息。PSC设置在MOC的一个属性上。记住第一条规则:设置过PSC属性的MOC调用saveContext的时候会将数据写到磁盘。
观察上图。无论何时你在这个单一的MOC上插入、更新或者删除一个实体,fetchedResultsController都会收到一个改变和更新表视图内容的通知。这与上下文的保存无关。您可以根据需要尽可能少地保存。Apple的模板在每次添加实体时保存,并且在applicationWillTerminate中保存。
这种方法适用于大部分情况,但正如我上面提到的,它有两个问题。您可能希望重用相同的视图控制器来添加和编辑实体。因此,您可能希望在呈现VC之前创建一个新实体,以便填充。这将导致更新通知触发对fetchedResultsController的更新,即在呈现用于添加或编辑的模态视图控制器之前,会短暂的出现空行。
如果在saveContext之前产生的更新太大,而且保存时间超过1/60秒,那么第二个问题会很明显。因为这种情况下,用户界面将被阻塞,直到保存完成,并且在滚动时有明显的跳转。
这两个问题都可以通过使用多上下文来解决。
传统的多上下文方式
将每个上下文视为一个临时的暂存器。iOS5之前,你能监听其他上下文的改变,并且通过通知在主线上下文合并其他上下文的变更。一个典型的设计如下图:
你将创建一个临时上下文,在后台队列使用。为临时上下文设置和主线程上下文一样的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指定。注意下图,我添加了多个子上下文,他们拥有同一个主队列父上文。
当子上下文保存时,父上文就会获取这些变化,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保持平滑。
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/