1. 程式人生 > >Core Data 網路應用例項

Core Data 網路應用例項

幾乎每一個應用開發者都需要經歷的就是將從 web service 獲取到的資料轉變到 Core Data 中。這篇文章闡述瞭如何去做。我們在這裡討論的每一個問題在之前的文章中都已經描述過了,並且 Apple 在他們的文件中也提過。然而,從頭到尾回顧一遍對我們來說還是很有益的。

計劃

我們將會建立一個簡單、只讀的應用程式,用來顯示 CocoaPods 說明的完整列表。這些說明都顯示在 table view 中,所有 pod 的說明都是以分頁的形式,從 web service 取得,並以 JSON 物件返回。

我們這樣來做

  1. 首先,我們建立一個 PodsWebservice 類,用來從 web service 請求所有的說明。
  2. 接著,建立一個 Importer 物件取出說明並將他們匯入 Core Data。
  3. 最終,我們展示如何讓最重要的工作在後臺執行緒中執行。

從 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 程式。