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