coredata的并发处理
“我想要高可响应性的app,它允许我即使离线时候也能浏览数据”-我们常常听到有人这么说。
讽刺的是,当用coredata处理数据的时候,它会成为你的应用的核心。除了管理你的app内存中持有数据的方式,coredata还处理高级数据查询(NSPredicate),懒加载(faults),undo和redo支持,表迁移,UI整合(NSFetchedResultsController),合并策略以及其他相关事情。
Coredata可以是你的朋友,也可以是你的敌人,这取决于你使用它的方式:如果在主线程处理,coredata会显著的抑制程序性能。但是,如果你想异步的使用coredata,你需要知道几种模式。
Coredata设计
尽管有人说coredata有比较陡的学习曲线,不过一旦你理解了这个技术它就会变得很容易理解。这个技术就是关于如何管理你的模型的对象流。Coredata的结构见下图:
通常SQLite数据库是用作备份数据存储,但是你也能指定“原子性”或者“仅内存”这两种储存类型。要注意的是尽管在OSX上coredata支持XML数据存储,但是在iOS上XML存储并不可用。
大致上,一个NSManagedObject实例可以被看做数据库里表里的一条记录。这个表的元数据被存储在NSEntityDescriptor对象中。NSManagedObjectContext是NSManagedObject对象们的数据池。它负责NSManagedObject的生命周期,以及从底层数据存储获取数据对象,持久化,object faulting, undo/redo支持。
一个NSManagedObjectContext必须工作在一个队列上。可通过两种方式来达成。第一种方式就是简单的只在创建NSManagedObjectContext的队列中来使用它。这被称作线程约束。
dispatch_async(queue, ^{
NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] init];
// moc usage
});
第二种方式是将NSManagedObjectContext初始化为concurrency类型,然后通过调用performBlock:^ 或者 performBlockAndWait:^来使用它。如下:
NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateConcurrencyType];
[moc performBlock:^{
// use moc in this block
}];
一共有三种异步类型可用:
- NSConfinementConcurrencyType - 尽管很多人使用,但是这个类型已经被苹果官方淘汰了。这个类型的意思是:你必须手动保证MOC在创建它的队列内使用。这种情况下,你应该使用init方法来做初始化,而不是initWithConcurrencyType方法。
- NSPrivateQueueConcurrencyType - 意味着[moc performBlock:]将在后台队列中执行。
- NSMainQueueConcurrencyType - 意味着[moc performBlock:]将在主队列中执行。
NSManagedObject不会被记录到持久化存储中,直到NSManagedObjectContext的save方法被调用。
NSPersistentStoreCoordinator的作用是绑定持久化存储和NSManagedObjectContext。通常,NSPersistentStoreCoordinator有一个持久化存储和多个NSManagedObjectContext,不过,它其实也可以同时处理多个持久化存储。你可以认为NSPersistentStoreCoordinator是持久化存储与NSManagedObject之间的中间层。
要清楚的是,UI更新是在主线程中发生,所以在主线程中的coredata操作都会产生影响,并且可能导致视觉问题或者性能问题。原因是所有UI处理,绘制和用户事件(点击,手势)都在主线程中进行。运行任何其他事情在主线程上都会对app性能产生影响。 If you want to know more about why it is a “failed dream” to create multithreaded UI on any platform, read “Multithreaded toolkits: A failed dream?”.
处理coredata有多重不同的方式。下面是几种实践中比较通用的处理方案。
方案 #1
就是把所有东西都放在主线程中处理。
第一种方法是相当简单的一个:把所有事情放在主线程处理。你需要Xcode生成的默认的CoreData堆栈。下图是这个堆栈的样子:
【图片】
这个唯一的NSManagedObjectContext是被用来UI处理和记录数据到数据库中。使用范例如下:
1.从服务器获取数据
2.只要数据返回,就展现某种等待提示器
3.存储获得的数据到数据库中(插入,更新,删除)
4.隐藏等待提示器并展示新数据。
然而作为一个非常简单的解决方案,负面影响就是在数据库记录数据的时候,由于它使用主线程在数据库记录新数据,似的app不可用。幸运的是,知名的MBProgressHUD库以一种聪明的方式实现,它能在主线程被coredata任务使用的时候在主线程展示动画。
方案 #2
将后台写任务从主线程中分离。
【图片】
这里,一个工作context被引入,用来存储从服务端收到的数据。一旦数据存储完成,主context可以以下面两种方式与工作context进行合并:
1.一旦工作context完成操作,它将通知主context,这之后工作线程可以通过调用reset方法来进行重置。
2.工作context触发mergeChangesFromContextDidSaveNotification通知后,主context将进行合并。工作context运行save方法后会触发上述通知。
这种方法的负面影响是开发者需要手动进行context间的合并。
方案 #3
利用NSManagedObjectContext间的父子关系。
在iOS5上,Apple在NSManagedObjectContext间引入了父子关系。使用这个可以帮助同步context之间的数据。下面是一个可能的方案:
【图片】
这个方案拥有在后台队列创建管理对象(NSManagedObject)的好处。但是在调用save方法后,所有数据对象将传给父context(主队列),而且真实的写数据库将依旧在主线程中发生。所以尽管本方案比方案1略强,但是它仍然对主线程有影响。
方案 #4
与方案3反向的管理对象上下文。
【图片】
流行库Restkit就是使用的这种结构,这此方案下,一个私有管理对象上下文被用于存储从服务端获取的数据。一旦完成,主context通过NSManagedObjectContextDidSaveNotification消息得到通知,于是主context更新到了它的父context(工作context)的数据。
在主context用于存储数据时候(可以处理较小量的数据),由于在主context与工作context间的父子关系,更改将被自动合并到私有context。
方案 #5
扩展父子关系的使用
【图片】
这个方案很有趣。作为对于方案4的补充,本方案引入了一个新的并发context,这个context是主context的子context。接下来我们来看看对应的数据流:
1.数据是异步获取的,当coredata在进行处理时候用户依然可以使用app。
2.服务端数据到达后,工作context被用来在数据库中存储数据。既然存储发生在私有队列,所以用户依然可以和之前一样使用app。
3.在工作context调用save方法后,数据就会被合并到主context。现在这个合并是在主队列发生的,但是由于合并是在内存中完成的,所以这是并不会产生严重的性能问题。
4.最终,数据被传递到另一个工作context,由这个工作context来真实的把数据写到存储里。
这种方法的负面影响是,由于有3个context进行合并,所有这些合并工作对app性能有一个可察觉的负面影响。
方案 #6
每个controller里都有一个管理对象context
如果你有很多工作context或者他们存储了大量的数据,主context与这些工作context间的数据合并就发生在主线程,这可能导致app不响应。
另外,主context在持有很多数据时候会变得很重。随着时间继续,对象会不停的加入context中。如果你想清除它,你必须在主context中调用reset方法。但是,如果你有多个视图控制器在屏幕上课件,这种情况在iPad上比较常见,你就需要对这个reset操作小心谨慎了,以免某个视图控制器获得到已经被重置的数据。
【图片】
这个解决方案就是你简单的不合并任何事情到主context中。你不一定只能有一个主context!与所有view controller只有一个主context相反,你可以尝试这种方法:每个view controller都有自己的NSManagedObjectContext。方法如下:
1.当数据从服务器获得后,使用工作context来存储数据。
2.一旦存储完成,就触发一个通知,比如类似MyAppUsersSavedNotification(假设你正在存储user列表数据到数据库中)。
3.显示user列表的控制器MyAppUsersViewController收到消息并执行下述代码:
[self.managedObjectContext reset];
self.fetchedResultsController = nil;
[self.tableView reloadData];
这样,这个view controller的NSFetchedResultsController将会被重新创建(很多例子中,假设你也有懒获取器),它会从coredata获取最新的数据。尽管这个操作是在主队列中进行的,但这应该是相当快速的,只要别忘记设置NSFetchRequest上fetchBatchSize。另外,也要考虑你真实需要的数据,如果你不需要所有字段,那么你应该只使用NSFetchRequest中的propertiesToFetch字段来获取你需要的字段。
哪个更好?
也许你希望找到一种适用所有情况的解决方案。不幸的是,通常来说,最佳方案取决于你的需求。对于简单的使用场景来说你使用方案1即可,但是对于一些更重的使用场景,我们建议你使用RestKit和那些采用了方案4的解决方案。