几乎每一个应用开发者都需要经历的就是将从 web service 获取到的数据转变到 Core Data 中。这篇文章阐述了如何去做。我们在这里讨论的每一个问题在之前的文章中都已经描述过了,并且 Apple 在他们的文档中也提过。然而,从头到尾回顾一遍对我们来说还是很有益的。
程序所有的代码都在 GitHub 上。
我们将会建立一个简单、只读的应用程序,用来显示 CocoaPods 说明的完整列表。这些说明都显示在 table view 中,所有 pod 的说明都是以分页的形式,从 web service 取得,并以 JSON 对象返回。
我们这样来做
PodsWebservice
类,用来从 web service 请求所有的说明。Importer
对象取出说明并将他们导入 Core Data。首先,创建一个单独的类从 web service 取得数据是很不错的。我们已经写了一个简单的 web server 示例,用来获取 CocoaPods 说明并将它们生成 JSON;请求 /specs
这个 URL 会返回一个按字母排序的 pod 说明列表。web service 是分页的,所以我们需要分开请求每一页。一个响应的示例如下:
{
"number_of_pages": 559,
"result": [{
"authors": { "Ash Furrow": "ash@ashfurrow.com" },
"homepage": "https://github.com/500px/500px-iOS-api",
"license": "MIT",
"name": "500px-iOS-api",
...
我们想要创建只有一个 fetchAllPods:
方法的类,它有一个回调 block,这将会被每一个页面调用。这也可以通过代理实现;但为什么我们选择用 block,你可以读一读这篇有关消息传递机制的文章。
@interface PodsWebservice : NSObject
- (void)fetchAllPods:(void (^)(NSArray *pods))callback;
@end
这个回调会被每个页面调用。实现这个方法很简单。我们创建一个帮助方法,fetchAllPods:page:
,它会为一个页面取得所有的 pods,一旦加载完一页就让它再调用自己。注意一下,为了简洁,我们这里不考虑处理错误,但是你可以在 GitHub 上完整的项目中看到。处理错误总是很重要的,至少打印出错误,这样你可以很快检查到哪些地方没有像预期一样工作:
- (void)fetchAllPods:(void (^)(NSArray *pods))callback page:(NSUInteger)page
{
NSString *urlString = [NSString stringWithFormat:@"http://localhost:4567/specs?page=%d", page];
NSURL *url = [NSURL URLWithString:urlString];
[[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
if ([result isKindOfClass:[NSDictionary class]]) {
NSArray *pods = result[@"result"];
callback(pods);
NSNumber* numberOfPages = result[@"number_of_pages"];
NSUInteger nextPage = page + 1;
if (nextPage < numberOfPages.unsignedIntegerValue) {
[self fetchAllPods:callback page:nextPage];
}
}
}] resume];
}
要做的就是这些了。我们解析 JSON,做一些非常粗糙的检查(验证结果是一个字典),然后调用回调函数。
现在我们可以将 JSON 装进我们的 Core Data store 中了。为了分清,我们创建一个 Importer
对象来调用 web service,并且创建或者更新对象。将这些放到一个单独的类中很不错,因为这样我们的 web service 和 Core Data 部分完全解耦。如果我们想要给 store 提供一个不同的 web service 或者在别的某个地方重用 web service,我们现在并不需要手动处理这两种情况。同时,不要在 view controller 中编写逻辑代码,以后我们可以在别的 app 中更容易复用这些组件。
我们的 Importer
有两个方法:
@interface Importer : NSObject
- (id)initWithContext:(NSManagedObjectContext *)context
webservice:(PodsWebservice *)webservice;
- (void)import;
@end
通过初始化方法将 context 注入到对象中是一个非常强有力的技巧。当编写测试的时候,我们可以很容易的注入一个不同的 context。同样适用于 web service:我们可以很容易的用一个不同的对象模拟 web service。
import
方法负责处理逻辑。我们调用 fetchAllPods:
方法,并且对于每一批 pod 说明,我们都会将它们导入到 context 中。通过将逻辑代码包装到 performBlock:
,context 会确保所有的事情都在正确的线程中执行。然后我们迭代这些说明,并且会为每一个说明生成一个唯一标识符(这些标识符可以是任何独一无二的,只要能确定到唯一一个 model object,正如在 Drew 的文章中解释那样。然后我们试着找到 model object,如果不存在则创建一个。loadFromDictionary:
方法需要一个 JSON 字典,并根据字典中的值更新 model object:
- (void)import
{
[self.webservice fetchAllPods:^(NSArray *pods)
{
[self.context performBlock:^
{
for(NSDictionary *podSpec in pods) {
NSString *identifier = [podSpec[@"name"] stringByAppendingString:podSpec[@"version"]];
Pod *pod = [Pod findOrCreatePodWithIdentifier:identifier inContext:self.context];
[pod loadFromDictionary:podSpec];
}
}];
}];
}
上面的代码中有很多地方要注意。首先,查找或创建方法的效率是非常低下的。在生产环境的代码中,你需要批量处理 pods 并且同时找到他们,正如在《导入大数据集》中「高效地导入数据」这一节中所解释的那样。
第二,我们直接在 Pod
类(managed object 的子类)中创建 loadFromDictionary:
。这意味着我们的 model object 知道 web service。在真实的代码中,我们很有可能将这些放到一个类别中,这样这两个很完美的分开了。对于这个示例,这无关要紧。
在写上面的代码时,我们会先在在主 managed object context 中拥有一切需要的数据。我们的应用在 table view 控制器中使用一个 fetched results controller 来显示所有的 pods。当 managed object context 中的数据改变时,fetched results controller 自动更新 data model。然而,在主 managed object context 中处理导入数据并不是最优的。主线程可能被堵塞,UI 可能没有反应。大多数时候,在主线程中处理的工作应该是最小限度的,并且造成的延迟应当难以察觉。如果你的情况正是这样,那非常好。然而,如果我们想要做些额外的努力,我们可以在后台线程中处理导入操作。
Apple 在 WWDC 会议以及官方的《Core Data 编程指南》文档的「Concurrency with Core Data」 一节中,对于并发的 Core Data,推荐给开发者两种选择。这两种都需要独立的 managed object contexts,它们要么共享同样的 persistent store coordinator,要么不共享。在处理很多改变时,拥有独立的 persistent store coordinators 提供更出色的性能,因为仅需要的锁只是在 sqlite 级别。拥有共享的 persistent store coordinator 也就意味着拥有共享缓存,当你没有做出很多改变时,这会很快。所以,根据你的情况而定,你需要衡量哪种方案更好,然后选择是否需要一个共享的 persistent store coordinator。当主 context 是只读的情况下,根本不需要锁,因为 iOS 7 中的 sqlite 有写前记录功能并且支持多重读取和单一写入。然而,对于我们的示范目的,我们会使用完全独立堆栈的处理方式。我们使用下面的代码设置一个 managed object context:
- (NSManagedObjectContext *)setupManagedObjectContextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType
{
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:concurrencyType];
managedObjectContext.persistentStoreCoordinator =
[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
NSError* error;
[managedObjectContext.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:self.storeURL
options:nil
error:&error];
if (error) {
NSLog(@"error: %@", error.localizedDescription);
}
return managedObjectContext;
}
然后我们调用这个方法两次,一次是为主 managed object context,一次是为后台 managed object context:
self.managedObjectContext = [self setupManagedObjectContextWithConcurrencyType:NSMainQueueConcurrencyType];
self.backgroundManagedObjectContext = [self setupManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType];
注意传递的参数 NSPrivateQueueConcurrencyType
告诉 Core Data 创建一个独立队列,这将确保后台 managed object context 的运行发生在一个独立的线程中。
现在就剩一步了:每当后台 context 保存后,我们需要更新主线程。我们在之前第 2 期的这篇文章中描述了如何操作。我们注册一下,当 context 保存时得到一个通知,如果是后台 context,调用 mergeChangesFromContextDidSaveNotification:
方法。这就是我们要做的所有事情:
[[NSNotificationCenter defaultCenter]
addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification* note) {
NSManagedObjectContext *moc = self.managedObjectContext;
if (note.object != moc) {
[moc performBlock:^(){
[moc mergeChangesFromContextDidSaveNotification:note];
}];
}
}];
这儿还有一个小忠告:mergeChangesFromContextDidSaveNotification:
是在 performBlock:
中发生的。在我们这个情况下,moc
是主 managed object context,因此,这将会阻塞主线程。
注意你的 UI(即使是只读的)必须有能力处理对象的改变,或者事件的删除。Brent Simmons 最近写了两篇文章,分别是 《Why Use a Custom Notification for Note Deletion》 和 《Deleting Objects in Core Data》。这些文章解释说明了如何面对这些情况,如果你在你的 UI 中显示一个对象,这个对象有可能会发生改变或者被删除。
你可能觉得上面讲的看起来非常简单,这是因为仅有的写操作是在后台线程进行的。在我们当前的应用中,我们没有处理其他方面的合并;并没有来自主 managed object context 中的改变。为了增加这个,你可以采用不少策略。Drew 的这篇文章很好的阐述了相关的方法。
根据你的需求,一个非常简单的模式或许是这样:不管用户何时改变 UI 中的某些东西,你并不改变 managed object context。相反,你去调用 web service。如果成功了,你可以从 web service 中得到改变,然后更新你的后台 context。这些改变随后回被传送到主 context。这样做有两个弊端:用户可能需要一段时间才能看到 UI 的改变,并且如果用户未联网,他将不能改变任何东西。在 Florian 的文章中,描述了我们如何使用不同策略让应用在离线时也能工作。
如果你正在处理合并,你也需要定义一个合并原则。这又是根据特定使用情况而定的。如果合并失败了你可能需要抛出一个错误,或者总是给某一个 managed object context 优先权。NSMergePolicy 类描述出了可能的选择。
我们已经看到如何实现一个简单的只读应用,这个应用能将从 web service 取得的大量数据导入到 Core Data。通过使用后台 managed object context,我们已经建立了一个不会阻塞 UI(除非正在处理合并)的 Core Data 程序。