幾乎每一個應(yīng)用開發(fā)者都需要經(jīng)歷的就是將從 web service 獲取到的數(shù)據(jù)轉(zhuǎn)變到 Core Data 中。這篇文章闡述了如何去做。我們在這里討論的每一個問題在之前的文章中都已經(jīng)描述過了,并且 Apple 在他們的文檔中也提過。然而,從頭到尾回顧一遍對我們來說還是很有益的。
程序所有的代碼都在 GitHub 上。
我們將會建立一個簡單、只讀的應(yīng)用程序,用來顯示 CocoaPods 說明的完整列表。這些說明都顯示在 table view 中,所有 pod 的說明都是以分頁的形式,從 web service 取得,并以 JSON 對象返回。
我們這樣來做
PodsWebservice 類,用來從 web service 請求所有的說明。Importer 對象取出說明并將他們導(dǎo)入 Core Data。首先,創(chuàng)建一個單獨(dú)的類從 web service 取得數(shù)據(jù)是很不錯的。我們已經(jīng)寫了一個簡單的 web server 示例,用來獲取 CocoaPods 說明并將它們生成 JSON;請求 /specs 這個 URL 會返回一個按字母排序的 pod 說明列表。web service 是分頁的,所以我們需要分開請求每一頁。一個響應(yīng)的示例如下:
{
"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",
...
我們想要創(chuàng)建只有一個 fetchAllPods: 方法的類,它有一個回調(diào) block,這將會被每一個頁面調(diào)用。這也可以通過代理實(shí)現(xiàn);但為什么我們選擇用 block,你可以讀一讀這篇有關(guān)消息傳遞機(jī)制的文章。
@interface PodsWebservice : NSObject
- (void)fetchAllPods:(void (^)(NSArray *pods))callback;
@end
這個回調(diào)會被每個頁面調(diào)用。實(shí)現(xiàn)這個方法很簡單。我們創(chuàng)建一個幫助方法,fetchAllPods:page:,它會為一個頁面取得所有的 pods,一旦加載完一頁就讓它再調(diào)用自己。注意一下,為了簡潔,我們這里不考慮處理錯誤,但是你可以在 GitHub 上完整的項(xiàng)目中看到。處理錯誤總是很重要的,至少打印出錯誤,這樣你可以很快檢查到哪些地方?jīng)]有像預(yù)期一樣工作:
- (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,做一些非常粗糙的檢查(驗(yàn)證結(jié)果是一個字典),然后調(diào)用回調(diào)函數(shù)。
現(xiàn)在我們可以將 JSON 裝進(jìn)我們的 Core Data store 中了。為了分清,我們創(chuàng)建一個 Importer 對象來調(diào)用 web service,并且創(chuàng)建或者更新對象。將這些放到一個單獨(dú)的類中很不錯,因?yàn)檫@樣我們的 web service 和 Core Data 部分完全解耦。如果我們想要給 store 提供一個不同的 web service 或者在別的某個地方重用 web service,我們現(xiàn)在并不需要手動處理這兩種情況。同時(shí),不要在 view controller 中編寫邏輯代碼,以后我們可以在別的 app 中更容易復(fù)用這些組件。
我們的 Importer 有兩個方法:
@interface Importer : NSObject
- (id)initWithContext:(NSManagedObjectContext *)context
webservice:(PodsWebservice *)webservice;
- (void)import;
@end
通過初始化方法將 context 注入到對象中是一個非常強(qiáng)有力的技巧。當(dāng)編寫測試的時(shí)候,我們可以很容易的注入一個不同的 context。同樣適用于 web service:我們可以很容易的用一個不同的對象模擬 web service。
import 方法負(fù)責(zé)處理邏輯。我們調(diào)用 fetchAllPods: 方法,并且對于每一批 pod 說明,我們都會將它們導(dǎo)入到 context 中。通過將邏輯代碼包裝到 performBlock:,context 會確保所有的事情都在正確的線程中執(zhí)行。然后我們迭代這些說明,并且會為每一個說明生成一個唯一標(biāo)識符(這些標(biāo)識符可以是任何獨(dú)一無二的,只要能確定到唯一一個 model object,正如在 Drew 的文章中解釋那樣。然后我們試著找到 model object,如果不存在則創(chuàng)建一個。loadFromDictionary: 方法需要一個 JSON 字典,并根據(jù)字典中的值更新 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];
}
}];
}];
}
上面的代碼中有很多地方要注意。首先,查找或創(chuàng)建方法的效率是非常低下的。在生產(chǎn)環(huán)境的代碼中,你需要批量處理 pods 并且同時(shí)找到他們,正如在《導(dǎo)入大數(shù)據(jù)集》中「高效地導(dǎo)入數(shù)據(jù)」這一節(jié)中所解釋的那樣。
第二,我們直接在 Pod 類(managed object 的子類)中創(chuàng)建 loadFromDictionary:。這意味著我們的 model object 知道 web service。在真實(shí)的代碼中,我們很有可能將這些放到一個類別中,這樣這兩個很完美的分開了。對于這個示例,這無關(guān)要緊。
在寫上面的代碼時(shí),我們會先在在主 managed object context 中擁有一切需要的數(shù)據(jù)。我們的應(yīng)用在 table view 控制器中使用一個 fetched results controller 來顯示所有的 pods。當(dāng) managed object context 中的數(shù)據(jù)改變時(shí),fetched results controller 自動更新 data model。然而,在主 managed object context 中處理導(dǎo)入數(shù)據(jù)并不是最優(yōu)的。主線程可能被堵塞,UI 可能沒有反應(yīng)。大多數(shù)時(shí)候,在主線程中處理的工作應(yīng)該是最小限度的,并且造成的延遲應(yīng)當(dāng)難以察覺。如果你的情況正是這樣,那非常好。然而,如果我們想要做些額外的努力,我們可以在后臺線程中處理導(dǎo)入操作。
Apple 在 WWDC 會議以及官方的《Core Data 編程指南》文檔的「Concurrency with Core Data」 一節(jié)中,對于并發(fā)的 Core Data,推薦給開發(fā)者兩種選擇。這兩種都需要獨(dú)立的 managed object contexts,它們要么共享同樣的 persistent store coordinator,要么不共享。在處理很多改變時(shí),擁有獨(dú)立的 persistent store coordinators 提供更出色的性能,因?yàn)閮H需要的鎖只是在 sqlite 級別。擁有共享的 persistent store coordinator 也就意味著擁有共享緩存,當(dāng)你沒有做出很多改變時(shí),這會很快。所以,根據(jù)你的情況而定,你需要衡量哪種方案更好,然后選擇是否需要一個共享的 persistent store coordinator。當(dāng)主 context 是只讀的情況下,根本不需要鎖,因?yàn)?iOS 7 中的 sqlite 有寫前記錄功能并且支持多重讀取和單一寫入。然而,對于我們的示范目的,我們會使用完全獨(dú)立堆棧的處理方式。我們使用下面的代碼設(shè)置一個 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;
}
然后我們調(diào)用這個方法兩次,一次是為主 managed object context,一次是為后臺 managed object context:
self.managedObjectContext = [self setupManagedObjectContextWithConcurrencyType:NSMainQueueConcurrencyType];
self.backgroundManagedObjectContext = [self setupManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType];
注意傳遞的參數(shù) NSPrivateQueueConcurrencyType 告訴 Core Data 創(chuàng)建一個獨(dú)立隊(duì)列,這將確保后臺 managed object context 的運(yùn)行發(fā)生在一個獨(dú)立的線程中。
現(xiàn)在就剩一步了:每當(dāng)后臺 context 保存后,我們需要更新主線程。我們在之前第 2 期的這篇文章中描述了如何操作。我們注冊一下,當(dāng) context 保存時(shí)得到一個通知,如果是后臺 context,調(diào)用 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:中發(fā)生的。在我們這個情況下,moc 是主 managed object context,因此,這將會阻塞主線程。
注意你的 UI(即使是只讀的)必須有能力處理對象的改變,或者事件的刪除。Brent Simmons 最近寫了兩篇文章,分別是 《Why Use a Custom Notification for Note Deletion》 和 《Deleting Objects in Core Data》。這些文章解釋說明了如何面對這些情況,如果你在你的 UI 中顯示一個對象,這個對象有可能會發(fā)生改變或者被刪除。
你可能覺得上面講的看起來非常簡單,這是因?yàn)閮H有的寫操作是在后臺線程進(jìn)行的。在我們當(dāng)前的應(yīng)用中,我們沒有處理其他方面的合并;并沒有來自主 managed object context 中的改變。為了增加這個,你可以采用不少策略。Drew 的這篇文章很好的闡述了相關(guān)的方法。
根據(jù)你的需求,一個非常簡單的模式或許是這樣:不管用戶何時(shí)改變 UI 中的某些東西,你并不改變 managed object context。相反,你去調(diào)用 web service。如果成功了,你可以從 web service 中得到改變,然后更新你的后臺 context。這些改變隨后回被傳送到主 context。這樣做有兩個弊端:用戶可能需要一段時(shí)間才能看到 UI 的改變,并且如果用戶未聯(lián)網(wǎng),他將不能改變?nèi)魏螙|西。在 Florian 的文章中,描述了我們?nèi)绾问褂貌煌呗宰寫?yīng)用在離線時(shí)也能工作。
如果你正在處理合并,你也需要定義一個合并原則。這又是根據(jù)特定使用情況而定的。如果合并失敗了你可能需要拋出一個錯誤,或者總是給某一個 managed object context 優(yōu)先權(quán)。NSMergePolicy 類描述出了可能的選擇。
我們已經(jīng)看到如何實(shí)現(xiàn)一個簡單的只讀應(yīng)用,這個應(yīng)用能將從 web service 取得的大量數(shù)據(jù)導(dǎo)入到 Core Data。通過使用后臺 managed object context,我們已經(jīng)建立了一個不會阻塞 UI(除非正在處理合并)的 Core Data 程序。