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