1. 程式人生 > >iCloud 和 Core Data

iCloud 和 Core Data

當喬布斯第一次在蘋果全球開發大會上介紹 iCloud 的時候,他將無縫同步的功能描述的太過完美,以至於讓人懷疑其是否真的能實現。但當你在 iOS 5iOS 6 系統中嘗試使用 iCloud Core Data 同步的時候你會對其真實情況瞭如指掌。

庫風格應用(譯者注:"盒子型別",比如 iPhoto )的同步中的問題導致很多開發者放棄支援 iCloud,而選擇一些其他的方案比如 SimperiumTICoreDataSyncWasabiSync

2013年初,在蘋果公司不透明及充滿 bug 的 iCloud Core Data 同步實現中掙扎多年後,開發者終於公開批判了這項服務的重大缺陷並將這個話題推上了

風口浪尖。 最終被 Ellis Hamburger 在一篇尖銳文章提出。

WWDC

蘋果也注意到了,很明顯這些事情必須改變。在 WWDC 2013,Nick Gillett 宣佈 Core Data 團隊花了一年時間專注於在 iOS 7 中解決一些 iCloud 最令人挫敗的漏洞,承諾大幅改善問題並且讓開發者更簡單的使用。“我們明顯減少了開發者所需要編寫的複雜程式碼的數量。” Nick Gillett在 [“What’s New in Core Data and iCloud”] 舞臺上講到。 在 iOS 7 中,Apple 專注於 iCloud 的速度,可靠性,和效能,事實上這卓有成效。

讓我們看看具體有哪些改變,以及如何在 iOS 7 應用程式實現 Core Data。

設定

要設定一個 iCloud Core Data 應用,你首先需要在你的應用中請求 iCloud 的訪問許可權,讓你的應用程式可以讀寫一個或多個開放性容器 (ubiquity containers),在 Xcode 5中你可以在你應用 target 的 “Capabilities” 選項卡中輕易完成著這一切。

在開放性容器內部,Core Data Framework 將會儲存所有的事務日誌 -- 記錄你的所有持久化的儲存 -- 為了跨裝置同步資料做準備。 Core Data 使用了一個被稱為多源複製(multi-master replication)的技術來同步 iOS 和 Macs 之間的資料。可持久化儲存的資料存在了每個裝置的 CoreDataUbiquitySupport

資料夾裡,你可以在應用沙盒中找到他。當用戶修改了 iCloud accounts,Core Data framework 會管理多個賬戶,而並不需要你自己去監聽NSUbiquityIdentityDidChangeNotification

每一個事務日誌都是一個plist檔案,負責實體的跟蹤插入,刪除以及更新。這些日誌會自動被系統按照一定基準合併。

  • NSPersistentStoreUbiquitousContentNameKey (NSString)
    給 iCloud 儲存空間指定一個名字(例如 @“MyAppStore”)

  • NSPersistentStoreUbiquitousContentURLKey (NSString, iOS 7 中可選) 給事務日誌指定一個二級目錄(例如 @"Logs")

  • NSPersistentStoreUbiquitousPeerTokenOption (NSString, 可選)
    為每個程式設定一個鹽,為了讓不同應用可以在同一個整合 iCloud 的裝置中分享 Core Data 資料 (比如@"d70548e8a24c11e3bbec425861b86ab6")

  • NSPersistentStoreRemoveUbiquitousMetadataOption (NSNumber (Boolean), 可選) 指定程式是否需要備份或遷移 iCloud 的元資料(例如 @YES)

  • NSPersistentStoreUbiquitousContainerIdentifierKey (NSString)
    指定一個容器,如果你的應用有多個容器定義在 entitlements 中(例如 @"com.company.MyApp.anothercontainer")

  • NSPersistentStoreRebuildFromUbiquitousContentOption (NSNumber (Boolean), 可選) 告訴 Core Data 抹除本地儲存資料並且用 iCoud 重建資料(例如 @YES)

只支援 iOS 7 的應用的唯一必填選項是 ContentNameKey,它是為了讓 Core Data 知道把日誌和元資料放在哪裡。在 iOS 7 中,你傳入 NSPersistentStoreUbiquitousContentNameKey 的字串值不應該包含'.'。 如果你的應用已經使用 Core Data 去儲存持久化資料,但是沒有實現 iCloud 同步,你只需要簡單加入 content name key 就能將儲存轉為可以使用 iCloud 的狀態,而無需關注有沒有活躍的 iCloud 賬戶。

為你的應用設定一個管理物件上下文簡單到只需要例項化一個 NSManagedObjectContext 並連同一個合併策略一併告訴你的持久化儲存。蘋果建議使用 NSMergeByPropertyObjectTrumpMergePolicy 作為合併策略,它會合並衝突,並給予記憶體中的變化的資料相較於磁碟資料更高的優先順序。

雖然 Apple 還沒有釋出官方的 iOS7 中 iCloud Core Data 的示例程式碼,但是 Apple 的 Core Data 團隊中的一個工程師在開發者論壇上提供了這個模板。我們稍微修改讓它更清晰:

#pragma mark - Notification Observers
- (void)registerForiCloudNotifications {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];

    [notificationCenter addObserver:self 
                           selector:@selector(storesWillChange:) 
                               name:NSPersistentStoreCoordinatorStoresWillChangeNotification 
                             object:self.persistentStoreCoordinator];

    [notificationCenter addObserver:self 
                           selector:@selector(storesDidChange:) 
                               name:NSPersistentStoreCoordinatorStoresDidChangeNotification 
                             object:self.persistentStoreCoordinator];

    [notificationCenter addObserver:self 
                           selector:@selector(persistentStoreDidImportUbiquitousContentChanges:) 
                               name:NSPersistentStoreDidImportUbiquitousContentChangesNotification 
                             object:self.persistentStoreCoordinator];
}

# pragma mark - iCloud Support

/// 在 -addPersistentStore: 使用這些配置
- (NSDictionary *)iCloudPersistentStoreOptions {
    return @{NSPersistentStoreUbiquitousContentNameKey: @"MyAppStore"};
}

- (void) persistentStoreDidImportUbiquitousContentChanges:(NSNotification *)notification {
    NSManagedObjectContext *context = self.managedObjectContext;

    [context performBlock:^{
        [context mergeChangesFromContextDidSaveNotification:changeNotification];
    }];
}

- (void)storesWillChange:(NSNotification *)notification {
    NSManagedObjectContext *context = self.managedObjectContext;

    [context performBlockAndWait:^{
        NSError *error;

        if ([context hasChanges]) {
            BOOL success = [context save:&error];

            if (!success && error) {
                // 執行錯誤處理
                NSLog(@"%@",[error localizedDescription]);
            }
        }

        [context reset];
    }];

    // 重新整理介面
}

- (void)storesDidChange:(NSNotification *)notification {
    // 重新整理介面
}

非同步持久化設定

在 iOS 7 中,使用 iCloud 選項來呼叫 addPersistentStoreWithType:configuration:URL:options:error: 幾乎可以瞬間返回儲存物件。[^1] 能做到這樣是因為它首先設定了一個內部‘回滾’儲存,利用本地儲存作為一個佔位符,同時由事務日誌和元資料來非同步地構建 iCloud 儲存。當回滾儲存有變化時,這些變化將在 iCloud 儲存被新增到 coordinator 時合併至其中。在完成回滾儲存的設定後,控制檯將會列印Using local storage: 1 ,當 iCloud 完全設定完後,你會看到 Using local storage: 0。 這句話的意思是 iCloud 儲存已經啟用,此後你可以通過監聽NSPersistentStoreDidImportUbiquitousContentChangesNotification看到來自 iCloud 的內容。

如果你的應用關注在不同儲存間的遷移,那麼你需要監聽 NSPersistentStoreCoordinatorStoresWillChangeNotification 和/或NSPersistentStoreCoordinatorStoresDidChangeNotification(將這些通知關聯到你的 coordinator,這樣就可以過濾其他和你無關的通知) 並且在 userInfo 中檢查 NSPersistentStoreUbiquitousTransitionTypeKey 的值, 這個數值是一個對應 NSPersistentStoreUbiquitousTransitionType 列舉型別的 NSNumber,在遷移已經發生時,這個值是NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted

邊緣情況

混淆 (Churn)

在 iOS 5 和 iOS 6 中測試 iCloud 時最嚴重的一個問題是重度使用者的賬號會遇到一種“混淆”的狀態,導致無法使用。同步將完全停止,甚至刪除開放性資料也無法使其正常工作。在 Lickability,我們親切地稱為這種狀態“f \ \ \ * ing bucket。”

在 iOS 7 中,系統提供了一個方法來真正移除全部的開放性儲存內容: +removeUbiquitousContentAndPersistentStoreAtURL:options:error:,這個方法對測試很有幫助,甚至在你應用中,當你使用者進入了一個不正常的狀態時,他們可以通過這個方法刪除所有資料,並重新來過。不過,需要指出的是:首先,這種方法是同步的。甚至在做網路操作的時候它也是同步的,因此它會花很長時間,並且在完成前也不會返回。第二,絕對不能在有永續性儲存 coordinators 活躍時執行此操作。這樣會造成很嚴重的問題,你的應用程式可能進入一個不可恢復的狀態,而且官方指導指出所有活躍的永續性儲存 coordinators 都應在使用這個方法前完全銷燬收回。

賬戶修改

iOS 5 系統中,使用者在切換 iCloud 賬戶或者禁用賬戶時,NSPersistentStoreCoordinator 中的資料會在應用無法知曉的情況下完全消失。事實上檢查一個賬號是否變更了的唯一的方法是呼叫 NSFileManager 中的 URLForUbiquityContainerIdentifier,這個方法可以建立一個開放性容器資料夾,而且需要數秒返回。在 iOS 6,這種情況隨著引進 ubiquityIdentityToken 和相應的NSUbiquityIdentityDidChangeNotification 之後得到改善。因為在 ubiquity id 變化的時候會發送通知,這就可以對應用賬戶的變更進行有效的確認並及時的發出提示。

然而,iOS 7 中這種轉換的情況就變得更加簡單,賬戶的切換是由 Core Data 框架來處理的,因此只要你的程式能夠正常響應 NSPersistentStoreCoordinatorStoresWillChangeNotificationNSPersistentStoreCoordinatorStoresDidChangeNotification 便可以在切換賬戶的時候流暢的更換資訊。檢查 userInfo 的字典中 NSPersistentStoreUbiquitousTransitionType 鍵將提供更多關於遷移的型別的細節。

在應用沙箱中框架會為每個賬戶管理各自獨立的持久化儲存,所以這就意味著如果使用者回到之前的賬戶,其資料會和之前離開時一樣,仍然可用。Core Data 現在也會在磁碟空間不足時管理對這些檔案進行的清理工作。

iCloud 的啟用與停用

在 iOS 7 中應用實現用一個開關用來切換啟用關閉 iCloud 變的非常容易,雖然對大部分應用來說這個功能不是很需要,因為在建立 NSPersistentStore 時候如果加入 iCloud 選項,那麼 API 現在將自動建立一個獨立的檔案結構,這意味著本地儲存和 iCloud 儲存共用相同的儲存 URL 和其他很多設定。這個選項將把 ubiquitous 元資料和儲存本身進行分離,並專門為遷移或者複製的場景進行了特殊設計。下面是一個示例:

- (void)migrateiCloudStoreToLocalStore {
    // 假設你只有一個儲存
    NSPersistentStore *store = [[_coordinator persistentStores] firstObject]; 

    NSMutableDictionary *localStoreOptions = [[self storeOptions] mutableCopy];
    [localStoreOptions setObject:@YES forKey:NSPersistentStoreRemoveUbiquitousMetadataOption];

    NSPersistentStore *newStore =  [_coordinator migratePersistentStore:store 
                                                                  toURL:[self storeURL] 
                                                                options:localStoreOptions 
                                                               withType:NSSQLiteStoreType error:nil];

    [self reloadStore:newStore];
}

- (void)reloadStore:(NSPersistentStore *)store {
    if (store) {
        [_coordinator removePersistentStore:store error:nil];
    }

    [_coordinator addPersistentStoreWithType:NSSQLiteStoreType 
                               configuration:nil 
                                         URL:[self storeURL] 
                                     options:[self storeOptions] 
                                       error:nil];
}

切換一個本地儲存到 iCloud 儲存是一個非常容易的事情,簡單到只需啟用 iCloud 選項,並且把擁有相同選項的可持久儲存加入到 coordinator 中。

外部檔案的引用

外部檔案的應用是一個在 iOS 5 中加入的 Core Data 新特性,允許大尺寸的二進位制自動儲存在 SQLite 資料庫之外的檔案系統中。 在我們測試中,當發生改變時,iCloud 並不知道如何解決依賴關係並會丟擲異常。如果你計劃使用 iCloud 同步 ,可以考慮在 iCloud entities 中取消這個選擇:

Core Data Modeler Checkbox

Model 版本

如果你計劃使用 iCloud,儲存的內容只能在未來相容自動輕量級遷移, 這意味著 Core Data 需要能推斷出對映,你也不能提供自己的對映模型。在未來只有對 Model 的簡單改變,比如新增和重新命名屬性,才能被支援。在考慮是否使用 Core Data 同步時,一定要考慮到你的 app 的 Model 在未來版本中改變的情況。

合併衝突

在任何同步系統中,伺服器和客戶端之前的檔案衝突是不可避免的。不同於 iCloud Data 文件同步的 APIs, iCloud 的 Core Data 整合並沒有明確允許處理本地儲存和事務日誌之間的衝突。這其實是因為 Core Data 已經支援通過實現 NSMergePolicy 的子類來自定義策合併策略。 如果你要處理衝突,建立 NSMergePolicy 的子類並且覆蓋 resolveConflicts:error: 來決定在衝突發生的時候做什麼。然後在你的 NSManagedObjectContext 子類中,讓mergePolicy 方法返回一個你自定義的策略的例項。

介面更新

很多庫風格應用同時顯示集合物件和一個物件的詳細資訊。 檢視是由 NSFetchedResultsController 例項自動從網路更新 Core Data 的資料然後重新整理。然而,您應該確保每一個詳細檢視正確監聽變化物件並使自己保持最新。如果你不這樣做, 將有顯示陳舊的資料的風險,或者更糟,你將覆蓋其他裝置修改的資料。

測試

本地網路和因特網同步

iCloud 守護程序將使用本地網路或使用因特網這兩種方式中的其中一種,來進行跨裝置的資料同步。守護程序檢測到兩個裝置時,也被稱為對等網路,在同一個區域網,將在內網快速傳輸。然而,如果在不同的網路,該系統將傳輸回滾事務日誌。這很重要,你必須在開發中對兩種情況進行大量的測試,以確保您的應用程式正常運作。在這兩種場景中,從備份儲存同步更改或過渡到 iCloud 有時需要比預期更長的時間,所以如果有什麼不工作,嘗試給它點時間。

模擬器中使用 iCloud

在 iOS 7 中最有用的更新就是 iCloud 終於可以在模擬器中使用。在以往的版本中,你只能在裝置中測試,這個限制使監聽開發的同步程序有點困難。現在你甚至可以在你的 Mac 和模擬器中進行資料同步。

在 Xcode 5 新增的 iCloud 除錯儀表中,你可以看到在你的應用程式的開放性儲存中的檔案,以及檢查它們的檔案傳輸狀態,比如 "Current", "Excluded", 和 "Stored in Cloud" 等。 對於更底層的除錯,可以把 -com.apple.coredata.ubiquity.logLevel 3 加入到啟動引數或者設定成使用者預設,以啟用詳細日誌。還可以考慮在 iOS 中安裝 iCloud 儲存除錯日誌配置檔案 以及新的 ubcontrol 命令列工具提供高質量錯誤報告到Apple 。你可以在你的裝置連入 iTunes 並同步後在 ~/Library/Logs/CrashReporter/MobileDevice/device-name/DiagnosticLogs 中獲取這些工具生成的日誌。

然而,iCloud Core Data 並不完全支援模擬器。在用實際裝置和模擬器測試傳輸時,似乎模擬器的 iCloud Core Data 只上傳更改,卻從不把它們抓取下來。雖然比起分別使用多個不同測試裝置來說,確實進步和方便了很多,但是 iOS 模擬器上的 iCloud Core Data 支援絕對還沒有完全成熟。

繼續改進

因為 iOS 7 中 APIs 和功能得到了極大的改善,那些在 iOS 5 和 iOS 6 上分發的帶有 iCloud Core Data 的應用的命運就顯得撲朔迷離了。 由於從 API 的角度來看它們完全不同(當然我們從功能角度也驗證了這一點),Apple 的建議對於那些需要傳統同步的應用來說並不那麼友好。Apple 清楚地開發者論壇 上建議,絕對不要在 iOS 7 和之前的裝置同步之間同步資料。

事實上,“任何時候你都不應該在 iOS 7 與 iOS 6 同步。iOS 6 將持續造成那些已經在 iOS 7 上修正了的 bug,這樣做將會會汙染 iCloud 賬戶。” 保證這種分離的最簡單的方法是簡單地改變你儲存中的 NSPersistentStoreUbiquitousContentNameKey,遵循規範進行命名。這樣保證從舊版本資料同步的方法是孤立的,並允許開發人員從老舊的實現中完全脫身。

釋出

釋出一個 iCloud Core Data 應用仍舊有很大的風險,你需要對所有的環節進行測試:賬戶轉換,iCloud 儲存空間耗盡,多種裝置,Model 的升級,以及裝置恢復等。儘管 iCloud 除錯儀表和 developer.icloud.com 對這些有所幫助,但依靠一個你完全無法控制的服務來發佈一個應用仍然需要那種縱身一躍入深淵的信念。

正如 Brent Simmon 提到的,釋出任意一種 iCloud Syncing 應用都會有限制,所以需要事先了解一下成本。像 Day One1Password 這樣的程式,會讓使用者選擇用 iCloud 還是 Dropbox 來同步他們的資料。對於很多使用者來說,沒什麼可以比一個獨立的賬戶更加簡易,但是一部分動手能力強的人喜歡更好的更全面的控制他們的資料。對於開發者而言,維持這種完全不同的資料庫同步系統在開發和測試的過程當中是十分繁瑣和超負荷的。

Bugs

一旦你測試並且釋出了你的 iCloud Core Data 應用,你很可能會遇到很多框架裡的 bug,最好的辦法是反饋這些 bug 的詳細資訊到 Apple,其中需要包含以下資訊:

  1. 完整的重現步驟
  2. 安裝了 iCloud 除錯配置並將 iCloud 除錯日誌輸出級別調為 3 的終端輸出
  3. 打包為 zip 的完整的開放性儲存內容

結論

在 iOS 5 和 6 中 iCloud Core Data 根本就沒法用這件事已經是不是一個祕密, Apple 的程式設計師自己都承認“在 iOS 5 和 6 中使用 Core Data + iCloud 時,存在重大的穩定性和長期可靠性的問題,要使用它的話請一定一定一定把應用設為 iOS 7 only“。一些高階的開發者,比如 Agile Tortoise 以及 Realmac Software,現在已經信任 iCloud Core Data,並把它整合到了他們的應用中。因為有著充分的考量和測試,你也應該這麼做了。

特別感謝 Andrew Harrison, Greg Pierce, and Paul Bruneau 對這篇文章的幫助

[^1]: 在之前的 OS 版本中,這個方法直到 iCloud 資料下載併合併到持久化儲存中前是不會返回的。這將造成大幅延遲,並意味著任何對這個方法的呼叫需要被派發到一個後臺的佇列中去。值得慶幸的是現在已經不再需要這麼做了。