當喬布斯第一次在蘋果全球開發(fā)大會上介紹 iCloud 的時候,他將無縫同步的功能描述的太過完美,以至于讓人懷疑其是否真的能實現(xiàn)。但當你在 iOS 5 和 iOS 6 系統(tǒng)中嘗試使用 iCloud Core Data 同步的時候你會對其真實情況了如指掌。
庫風(fēng)格應(yīng)用(譯者注:"盒子類型",比如 iPhoto )的同步中的問題導(dǎo)致很多開發(fā)者放棄支持 iCloud,而選擇一些其他的方案比如 Simperium,TICoreDataSync 和 WasabiSync。
2013年初,在蘋果公司不透明及充滿 bug 的 iCloud Core Data 同步實現(xiàn)中掙扎多年后,開發(fā)者終于公開批判了這項服務(wù)的重大缺陷并將這個話題推上了風(fēng)口浪尖。 最終被 Ellis Hamburger 在一篇尖銳文章提出。
蘋果也注意到了,很明顯這些事情必須改變。在 WWDC 2013,Nick Gillett 宣布 Core Data 團隊花了一年時間專注于在 iOS 7 中解決一些 iCloud 最令人挫敗的漏洞,承諾大幅改善問題并且讓開發(fā)者更簡單的使用。“我們明顯減少了開發(fā)者所需要編寫的復(fù)雜代碼的數(shù)量。” Nick Gillett在 [“What’s New in Core Data and iCloud”] 舞臺上講到。 在 iOS 7 中,Apple 專注于 iCloud 的速度,可靠性,和性能,事實上這卓有成效。
讓我們看看具體有哪些改變,以及如何在 iOS 7 應(yīng)用程序?qū)崿F(xiàn) Core Data。
要設(shè)置一個 iCloud Core Data 應(yīng)用,你首先需要在你的應(yīng)用中請求 iCloud 的訪問權(quán)限,讓你的應(yīng)用程序可以讀寫一個或多個開放性容器 (ubiquity containers),在 Xcode 5中你可以在你應(yīng)用 target 的 “Capabilities” 選項卡中輕易完成著這一切。
在開放性容器內(nèi)部,Core Data Framework 將會存儲所有的事務(wù)日志 -- 記錄你的所有持久化的存儲 -- 為了跨設(shè)備同步數(shù)據(jù)做準備。 Core Data 使用了一個被稱為多源復(fù)制(multi-master replication)的技術(shù)來同步 iOS 和 Macs 之間的數(shù)據(jù)。可持久化存儲的數(shù)據(jù)存在了每個設(shè)備的 CoreDataUbiquitySupport 文件夾里,你可以在應(yīng)用沙盒中找到他。當用戶修改了 iCloud accounts,Core Data framework 會管理多個賬戶,而并不需要你自己去監(jiān)聽NSUbiquityIdentityDidChangeNotification。
每一個事務(wù)日志都是一個plist文件,負責(zé)實體的跟蹤插入,刪除以及更新。這些日志會自動被系統(tǒng)按照一定基準合并。
在你設(shè)置iCloud的持久化存儲的時候,調(diào)用addPersistentStoreWithType:configuration:URL:options:error:或者 migratePersistentStore:toURL:options:withType:error:的時候注意需要設(shè)置一些選項:
NSPersistentStoreUbiquitousContentNameKey (NSString)
給 iCloud 存儲空間指定一個名字(例如 @“MyAppStore”)
NSPersistentStoreUbiquitousContentURLKey (NSString, iOS 7 中可選)
給事務(wù)日志指定一個二級目錄(例如 @"Logs")
NSPersistentStoreUbiquitousPeerTokenOption (NSString, 可選)
為每個程序設(shè)置一個鹽,為了讓不同應(yīng)用可以在同一個集成 iCloud 的設(shè)備中分享 Core Data 數(shù)據(jù) (比如@"d70548e8a24c11e3bbec425861b86ab6")
NSPersistentStoreRemoveUbiquitousMetadataOption (NSNumber (Boolean), 可選)
指定程序是否需要備份或遷移 iCloud 的元數(shù)據(jù)(例如 @YES)
NSPersistentStoreUbiquitousContainerIdentifierKey (NSString)
指定一個容器,如果你的應(yīng)用有多個容器定義在 entitlements 中(例如 @"com.company.MyApp.anothercontainer")
NSPersistentStoreRebuildFromUbiquitousContentOption (NSNumber (Boolean), 可選)
告訴 Core Data 抹除本地存儲數(shù)據(jù)并且用 iCoud 重建數(shù)據(jù)(例如 @YES)只支持 iOS 7 的應(yīng)用的唯一必填選項是 ContentNameKey,它是為了讓 Core Data 知道把日志和元數(shù)據(jù)放在哪里。在 iOS 7 中,你傳入 NSPersistentStoreUbiquitousContentNameKey 的字符串值不應(yīng)該包含'.'。 如果你的應(yīng)用已經(jīng)使用 Core Data 去存儲持久化數(shù)據(jù),但是沒有實現(xiàn) iCloud 同步,你只需要簡單加入 content name key 就能將存儲轉(zhuǎn)為可以使用 iCloud 的狀態(tài),而無需關(guān)注有沒有活躍的 iCloud 賬戶。
為你的應(yīng)用設(shè)置一個管理對象上下文簡單到只需要實例化一個 NSManagedObjectContext 并連同一個合并策略一并告訴你的持久化存儲。蘋果建議使用 NSMergeByPropertyObjectTrumpMergePolicy 作為合并策略,它會合并沖突,并給予內(nèi)存中的變化的數(shù)據(jù)相較于磁盤數(shù)據(jù)更高的優(yōu)先級。
雖然 Apple 還沒有發(fā)布官方的 iOS7 中 iCloud Core Data 的示例代碼,但是 Apple 的 Core Data 團隊中的一個工程師在開發(fā)者論壇上提供了這個模板。我們稍微修改讓它更清晰:
#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) {
// 執(zhí)行錯誤處理
NSLog(@"%@",[error localizedDescription]);
}
}
[context reset];
}];
// 刷新界面
}
- (void)storesDidChange:(NSNotification *)notification {
// 刷新界面
}
在 iOS 7 中,使用 iCloud 選項來調(diào)用 addPersistentStoreWithType:configuration:URL:options:error: 幾乎可以瞬間返回存儲對象。1 能做到這樣是因為它首先設(shè)置了一個內(nèi)部‘回滾’存儲,利用本地存儲作為一個占位符,同時由事務(wù)日志和元數(shù)據(jù)來異步地構(gòu)建 iCloud 存儲。當回滾存儲有變化時,這些變化將在 iCloud 存儲被添加到 coordinator 時合并至其中。在完成回滾存儲的設(shè)置后,控制臺將會打印Using local storage: 1 ,當 iCloud 完全設(shè)置完后,你會看到 Using local storage: 0。 這句話的意思是 iCloud 存儲已經(jīng)啟用,此后你可以通過監(jiān)聽NSPersistentStoreDidImportUbiquitousContentChangesNotification看到來自 iCloud 的內(nèi)容。
如果你的應(yīng)用關(guān)注在不同存儲間的遷移,那么你需要監(jiān)聽 NSPersistentStoreCoordinatorStoresWillChangeNotification 和/或NSPersistentStoreCoordinatorStoresDidChangeNotification(將這些通知關(guān)聯(lián)到你的 coordinator,這樣就可以過濾其他和你無關(guān)的通知) 并且在 userInfo 中檢查 NSPersistentStoreUbiquitousTransitionTypeKey 的值, 這個數(shù)值是一個對應(yīng) NSPersistentStoreUbiquitousTransitionType 枚舉類型的 NSNumber,在遷移已經(jīng)發(fā)生時,這個值是NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted。
在 iOS 5 和 iOS 6 中測試 iCloud 時最嚴重的一個問題是重度用戶的賬號會遇到一種“混淆”的狀態(tài),導(dǎo)致無法使用。同步將完全停止,甚至刪除開放性數(shù)據(jù)也無法使其正常工作。在 Lickability,我們親切地稱為這種狀態(tài)“f \ \ \ * ing bucket?!?/p>
在 iOS 7 中,系統(tǒng)提供了一個方法來真正移除全部的開放性存儲內(nèi)容: +removeUbiquitousContentAndPersistentStoreAtURL:options:error:,這個方法對測試很有幫助,甚至在你應(yīng)用中,當你用戶進入了一個不正常的狀態(tài)時,他們可以通過這個方法刪除所有數(shù)據(jù),并重新來過。不過,需要指出的是:首先,這種方法是同步的。甚至在做網(wǎng)絡(luò)操作的時候它也是同步的,因此它會花很長時間,并且在完成前也不會返回。第二,絕對不能在有持久性存儲 coordinators 活躍時執(zhí)行此操作。這樣會造成很嚴重的問題,你的應(yīng)用程序可能進入一個不可恢復(fù)的狀態(tài),而且官方指導(dǎo)指出所有活躍的持久性存儲 coordinators 都應(yīng)在使用這個方法前完全銷毀收回。
iOS 5 系統(tǒng)中,用戶在切換 iCloud 賬戶或者禁用賬戶時,NSPersistentStoreCoordinator 中的數(shù)據(jù)會在應(yīng)用無法知曉的情況下完全消失。事實上檢查一個賬號是否變更了的唯一的方法是調(diào)用 NSFileManager 中的 URLForUbiquityContainerIdentifier,這個方法可以創(chuàng)建一個開放性容器文件夾,而且需要數(shù)秒返回。在 iOS 6,這種情況隨著引進 ubiquityIdentityToken 和相應(yīng)的NSUbiquityIdentityDidChangeNotification 之后得到改善。因為在 ubiquity id 變化的時候會發(fā)送通知,這就可以對應(yīng)用賬戶的變更進行有效的確認并及時的發(fā)出提示。
然而,iOS 7 中這種轉(zhuǎn)換的情況就變得更加簡單,賬戶的切換是由 Core Data 框架來處理的,因此只要你的程序能夠正常響應(yīng) NSPersistentStoreCoordinatorStoresWillChangeNotification 和 NSPersistentStoreCoordinatorStoresDidChangeNotification 便可以在切換賬戶的時候流暢的更換信息。檢查 userInfo 的字典中 NSPersistentStoreUbiquitousTransitionType 鍵將提供更多關(guān)于遷移的類型的細節(jié)。
在應(yīng)用沙箱中框架會為每個賬戶管理各自獨立的持久化存儲,所以這就意味著如果用戶回到之前的賬戶,其數(shù)據(jù)會和之前離開時一樣,仍然可用。Core Data 現(xiàn)在也會在磁盤空間不足時管理對這些文件進行的清理工作。
在 iOS 7 中應(yīng)用實現(xiàn)用一個開關(guān)用來切換啟用關(guān)閉 iCloud 變的非常容易,雖然對大部分應(yīng)用來說這個功能不是很需要,因為在創(chuàng)建 NSPersistentStore 時候如果加入 iCloud 選項,那么 API 現(xiàn)在將自動建立一個獨立的文件結(jié)構(gòu),這意味著本地存儲和 iCloud 存儲共用相同的存儲 URL 和其他很多設(shè)置。這個選項將把 ubiquitous 元數(shù)據(jù)和存儲本身進行分離,并專門為遷移或者復(fù)制的場景進行了特殊設(shè)計。下面是一個示例:
- (void)migrateiCloudStoreToLocalStore {
// 假設(shè)你只有一個存儲
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 中。
外部文件的應(yīng)用是一個在 iOS 5 中加入的 Core Data 新特性,允許大尺寸的二進制自動存儲在 SQLite 數(shù)據(jù)庫之外的文件系統(tǒng)中。 在我們測試中,當發(fā)生改變時,iCloud 并不知道如何解決依賴關(guān)系并會拋出異常。如果你計劃使用 iCloud 同步 ,可以考慮在 iCloud entities 中取消這個選擇:
http://wiki.jikexueyuan.com/project/objc/images/10-6.png" alt="" />
如果你計劃使用 iCloud,存儲的內(nèi)容只能在未來兼容自動輕量級遷移, 這意味著 Core Data 需要能推斷出映射,你也不能提供自己的映射模型。在未來只有對 Model 的簡單改變,比如添加和重命名屬性,才能被支持。在考慮是否使用 Core Data 同步時,一定要考慮到你的 app 的 Model 在未來版本中改變的情況。
在任何同步系統(tǒng)中,服務(wù)器和客戶端之前的文件沖突是不可避免的。不同于 iCloud Data 文檔同步的 APIs, iCloud 的 Core Data 整合并沒有明確允許處理本地存儲和事務(wù)日志之間的沖突。這其實是因為 Core Data 已經(jīng)支持通過實現(xiàn) NSMergePolicy 的子類來自定義策合并策略。 如果你要處理沖突,創(chuàng)建 NSMergePolicy 的子類并且覆蓋 resolveConflicts:error: 來決定在沖突發(fā)生的時候做什么。然后在你的 NSManagedObjectContext 子類中,讓mergePolicy 方法返回一個你自定義的策略的實例。
很多庫風(fēng)格應(yīng)用同時顯示集合對象和一個對象的詳細信息。 視圖是由 NSFetchedResultsController 實例自動從網(wǎng)絡(luò)更新 Core Data 的數(shù)據(jù)然后刷新。然而,您應(yīng)該確保每一個詳細視圖正確監(jiān)聽變化對象并使自己保持最新。如果你不這樣做, 將有顯示陳舊的數(shù)據(jù)的風(fēng)險,或者更糟,你將覆蓋其他設(shè)備修改的數(shù)據(jù)。
iCloud 守護進程將使用本地網(wǎng)絡(luò)或使用因特網(wǎng)這兩種方式中的其中一種,來進行跨設(shè)備的數(shù)據(jù)同步。守護進程檢測到兩個設(shè)備時,也被稱為對等網(wǎng)絡(luò),在同一個局域網(wǎng),將在內(nèi)網(wǎng)快速傳輸。然而,如果在不同的網(wǎng)絡(luò),該系統(tǒng)將傳輸回滾事務(wù)日志。這很重要,你必須在開發(fā)中對兩種情況進行大量的測試,以確保您的應(yīng)用程序正常運作。在這兩種場景中,從備份存儲同步更改或過渡到 iCloud 有時需要比預(yù)期更長的時間,所以如果有什么不工作,嘗試給它點時間。
在 iOS 7 中最有用的更新就是 iCloud 終于可以在模擬器中使用。在以往的版本中,你只能在設(shè)備中測試,這個限制使監(jiān)聽開發(fā)的同步進程有點困難?,F(xiàn)在你甚至可以在你的 Mac 和模擬器中進行數(shù)據(jù)同步。
在 Xcode 5 新增的 iCloud 調(diào)試儀表中,你可以看到在你的應(yīng)用程序的開放性存儲中的文件,以及檢查它們的文件傳輸狀態(tài),比如 "Current", "Excluded", 和 "Stored in Cloud" 等。 對于更底層的調(diào)試,可以把 -com.apple.coredata.ubiquity.logLevel 3 加入到啟動參數(shù)或者設(shè)置成用戶默認,以啟用詳細日志。還可以考慮在 iOS 中安裝 iCloud 存儲調(diào)試日志配置文件 以及新的 ubcontrol 命令行工具提供高質(zhì)量錯誤報告到Apple 。你可以在你的設(shè)備連入 iTunes 并同步后在 ~/Library/Logs/CrashReporter/MobileDevice/device-name/DiagnosticLogs
中獲取這些工具生成的日志。
然而,iCloud Core Data 并不完全支持模擬器。在用實際設(shè)備和模擬器測試傳輸時,似乎模擬器的 iCloud Core Data 只上傳更改,卻從不把它們抓取下來。雖然比起分別使用多個不同測試設(shè)備來說,確實進步和方便了很多,但是 iOS 模擬器上的 iCloud Core Data 支持絕對還沒有完全成熟。
因為 iOS 7 中 APIs 和功能得到了極大的改善,那些在 iOS 5 和 iOS 6 上分發(fā)的帶有 iCloud Core Data 的應(yīng)用的命運就顯得撲朔迷離了。 由于從 API 的角度來看它們完全不同(當然我們從功能角度也驗證了這一點),Apple 的建議對于那些需要傳統(tǒng)同步的應(yīng)用來說并不那么友好。Apple 清楚地 在開發(fā)者論壇 上建議,絕對不要在 iOS 7 和之前的設(shè)備同步之間同步數(shù)據(jù)。
事實上,“任何時候你都不應(yīng)該在 iOS 7 與 iOS 6 同步。iOS 6 將持續(xù)造成那些已經(jīng)在 iOS 7 上修正了的 bug,這樣做將會會污染 iCloud 賬戶?!?保證這種分離的最簡單的方法是簡單地改變你存儲中的 NSPersistentStoreUbiquitousContentNameKey,遵循規(guī)范進行命名。這樣保證從舊版本數(shù)據(jù)同步的方法是孤立的,并允許開發(fā)人員從老舊的實現(xiàn)中完全脫身。
發(fā)布一個 iCloud Core Data 應(yīng)用仍舊有很大的風(fēng)險,你需要對所有的環(huán)節(jié)進行測試:賬戶轉(zhuǎn)換,iCloud 存儲空間耗盡,多種設(shè)備,Model 的升級,以及設(shè)備恢復(fù)等。盡管 iCloud 調(diào)試儀表和 developer.icloud.com 對這些有所幫助,但依靠一個你完全無法控制的服務(wù)來發(fā)布一個應(yīng)用仍然需要那種縱身一躍入深淵的信念。
正如 Brent Simmon 提到的,發(fā)布任意一種 iCloud Syncing 應(yīng)用都會有限制,所以需要事先了解一下成本。像 Day One 和 1Password 這樣的程序,會讓使用者選擇用 iCloud 還是 Dropbox 來同步他們的數(shù)據(jù)。對于很多使用者來說,沒什么可以比一個獨立的賬戶更加簡易,但是一部分動手能力強的人喜歡更好的更全面的控制他們的數(shù)據(jù)。對于開發(fā)者而言,維持這種完全不同的數(shù)據(jù)庫同步系統(tǒng)在開發(fā)和測試的過程當中是十分繁瑣和超負荷的。
一旦你測試并且發(fā)布了你的 iCloud Core Data 應(yīng)用,你很可能會遇到很多框架里的 bug,最好的辦法是反饋這些 bug 的詳細信息到 Apple,其中需要包含以下信息:
在 iOS 5 和 6 中 iCloud Core Data 根本就沒法用這件事已經(jīng)是不是一個秘密, Apple 的程序員自己都承認“在 iOS 5 和 6 中使用 Core Data + iCloud 時,存在重大的穩(wěn)定性和長期可靠性的問題,要使用它的話請一定一定一定把應(yīng)用設(shè)為 iOS 7 only“。一些高端的開發(fā)者,比如 Agile Tortoise 以及 Realmac Software,現(xiàn)在已經(jīng)信任 iCloud Core Data,并把它集成到了他們的應(yīng)用中。因為有著充分的考量和測試,你也應(yīng)該這么做了。
特別感謝 Andrew Harrison, Greg Pierce, and Paul Bruneau 對這篇文章的幫助
在之前的 OS 版本中,這個方法直到 iCloud 數(shù)據(jù)下載并合并到持久化存儲中前是不會返回的。這將造成大幅延遲,并意味著任何對這個方法的調(diào)用需要被派發(fā)到一個后臺的隊列中去。值得慶幸的是現(xiàn)在已經(jīng)不再需要這么做了。 ↩