Core Data 網路應用例項
幾乎每一個應用開發者都需要經歷的就是將從 web service 獲取到的資料轉變到 Core Data 中。這篇文章闡述瞭如何去做。我們在這裡討論的每一個問題在之前的文章中都已經描述過了,並且 Apple 在他們的文件中也提過。然而,從頭到尾回顧一遍對我們來說還是很有益的。
計劃
我們將會建立一個簡單、只讀的應用程式,用來顯示 CocoaPods 說明的完整列表。這些說明都顯示在 table view 中,所有 pod 的說明都是以分頁的形式,從 web service 取得,並以 JSON 物件返回。
我們這樣來做
- 首先,我們建立一個
PodsWebservice
類,用來從 web service 請求所有的說明。 - 接著,建立一個
Importer
物件取出說明並將他們匯入 Core Data。 - 最終,我們展示如何讓最重要的工作在後臺執行緒中執行。
從 Web Service 取得物件
首先,建立一個單獨的類從 web service 取得資料是很不錯的。我們已經寫了一個簡單的 web server 示例,用來獲取 CocoaPods 說明並將它們生成 JSON;請求 /specs
這個 URL 會返回一個按字母排序的 pod 說明列表。web service 是分頁的,所以我們需要分開請求每一頁。一個響應的示例如下:
{
"number_of_pages": 559,
"result": [{
"authors": { "Ash Furrow": " [email protected]" },
"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,做一些非常粗糙的檢查(驗證結果是一個字典),然後呼叫回撥函式。
將物件裝進 Core Data
現在我們可以將 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 中顯示一個物件,這個物件有可能會發生改變或者被刪除。
實現從 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 程式。