即便在推出 3 年后,iCloud 文檔存儲依然是一個充滿神秘、誤解和抱怨的話題。iCloud 同步經(jīng)常被批評不可靠且速度慢。雖然在 iCloud 的早期有一些嚴重的 bug,開發(fā)者們還是不得不學(xué)習(xí)有關(guān)文件同步的課程。文件同步事關(guān)重大,為應(yīng)用開發(fā)帶來了新方向 -- 一個經(jīng)常被低估的方向,比如進行同步服務(wù)相關(guān)的合作時,對于處理文件異步更改的需要。
本文會介紹幾個創(chuàng)建支持 iCloud 的應(yīng)用時可能會遇到的一些絆腳石。因為本文只會給出一些粗略的概述,所以如果你對 iCloud 文檔存儲還不熟悉,我們強烈建議你先閱讀 Apple iCloud companion guide。
iCloud 文檔存儲的核心思想非常簡單:每個應(yīng)用都有至少通往一個“魔法文件夾”的入口,該文件夾可以存儲文件并且隨后在所有注冊了同一個 iCloud 帳號的設(shè)備間同步。
與其他基于文件系統(tǒng)的同步服務(wù)相比,iCloud 文檔存儲得益于與 OS X 和 iOS 的深度整合。很多系統(tǒng)框架已經(jīng)被擴展以支持 iCloud。像 NSDocument 和 UIDocument 這樣的類被按照可以處理外部變化來進行設(shè)計。版本存儲和 NSFileVersion 處理同步?jīng)_突。Spotlight 被用來提供同步元數(shù)據(jù),比如文件傳輸進度或者云端文檔可用性。
寫一個簡單的基于文檔并開啟了 iCloud 的 OS X 應(yīng)用并不需要費多大力氣。實際上你并不需要關(guān)心任何 iCloud 內(nèi)部的工作,NSDocument 無償?shù)淖隽藥缀趺考虑椋簠f(xié)調(diào)文檔的 iCloud 訪問,自動觀察外部變化,觸發(fā)下載,處理沖突。它甚至提供了一個簡單的 UI 界面來管理云文檔。你需要做的所有事情就是創(chuàng)建一個 NSDocument 子類并實現(xiàn)讀取和寫入文檔內(nèi)容所需要的方法。
http://wiki.jikexueyuan.com/project/objc/images/10-7.png" alt="" />
然而,一旦脫離預(yù)設(shè)的路徑,你就需要了解的更多。例如,默認打開面板提供的單層文件夾以外的任何操作都需要手動完成??赡苣愕膽?yīng)用需要管理除了文檔內(nèi)容以外的文檔,比如像 Mail,iPhoto 或者 Ulysses (我們自己的app) 中做的那樣。這種時候,你不能依賴于 NSDocument,而需要自己實現(xiàn)它的功能。但為此你需要對 iCloud 提供的鎖和通知機制有一個深入的了解。
開發(fā)支持 iCloud 的 iOS 應(yīng)用同樣需要更多的工作和知識;雖然 UIDocument 仍然管理 iCloud 文件訪問和處理同步?jīng)_突,但缺乏管理文檔和文件夾的圖形界面。因為性能和存儲空間的原因,iOS 也不會自動從云端下載新文檔。你需要使用 Spotlight 來檢索最近變化的目錄并手動觸發(fā)下載。
任何符合 App Store 條件的應(yīng)用都可以使用 iCloud 文檔存儲。設(shè)置正確的授權(quán)后,就獲得了一個或多個所謂的“開放性容器”的訪問權(quán)限。這是蘋果用來稱呼“一個被 iCloud 管理和同步的目錄”的別稱。每一個開放性容器限定在一個 app id 內(nèi),由此讓每個用戶在每個應(yīng)用中有一份共享的存儲倉庫。有多個應(yīng)用的開發(fā)者可以指定同一個團隊的多個 app id,由此可以訪問多個容器。
NSFileManager 通過 URLForUbiquityContainerIdentifier: 提供每一個容器的 URL。在 OS X 系統(tǒng),可以通過打開 ~/Library/Mobile Documents 目錄來查看所有可用的開放性容器。
http://wiki.jikexueyuan.com/project/objc/images/10-8.png" alt="" />
通常每個開放性容器有兩個并發(fā)進程訪問。首先,有一個應(yīng)用呈現(xiàn)和操作容器內(nèi)的文檔。第二,有一個主要通過開放性守護 (Ubiquity Daemon ubd) 體現(xiàn)的 iCloud 架構(gòu)。iCloud 架構(gòu)等待應(yīng)用對文檔的更改并將其上傳至蘋果云服務(wù)器。同時也等待從 iCloud 上收到的更改并相應(yīng)修改容器的內(nèi)容。
由于兩個進程完全獨立于彼此工作,因此需某種形式的仲裁來避免資源競爭或丟失容器內(nèi)的文件更新的問題。應(yīng)用需要使用名為 文件協(xié)調(diào) file coordination 的概念來確保對于每一個獨立文件的訪問權(quán)。該訪問權(quán)由 NSFileCoordinator 類提供。概括來說,它為每個文件提供了一個簡單的 讀-寫 鎖。這個鎖由一個通知機制擴展,該機制用于用于改善訪問同一個文件的不同進程間的合作。
這個通知機制相比于簡單的文件鎖來說是有巨大的的好處,并且提供了無縫的用戶體驗。iCloud 可能會在任何時間把文檔用一個來自其他設(shè)備的新版本覆蓋。如果一個應(yīng)用當(dāng)前正在顯示同一個文檔,它必須從磁盤加載新版本并向用戶展示更新過的內(nèi)容。更新過程中,應(yīng)用可能需要鎖住用戶界面一段時間并隨后在此打開。甚至可能發(fā)生更壞的情況:應(yīng)用可能保留著未保存的內(nèi)容,這些內(nèi)容需要首先保存到磁盤上以便檢查同步?jīng)_突。最后,在網(wǎng)絡(luò)條件良好的時候 iCloud 會上傳文件最近的版本。因此必須能夠要求應(yīng)用立刻保存所有未保存的變更。
為了實現(xiàn)這個過程,文件協(xié)調(diào)伴隨著另一套名為 文件展示 (file presentation) 的機制。無論什么時候應(yīng)用打開并向用戶展示一個文件,這被稱為被稱作 展示文檔,并且應(yīng)該注冊一個實現(xiàn)了 NSFilePresenter 協(xié)議的對象。只要另一個進程通過一個文件協(xié)調(diào)訪問文件,文件展示者 (file presenter) 就會收到關(guān)于該文件的通知。這些通知被作為方法調(diào)用傳遞,這些方法在展示者指定的一個操作隊列(presentedItemOperationQueue)中異步執(zhí)行。
例如,在任何其他線程被允許開始一個讀取操作前,文件展示者被要求保存任何未保存的變化。這些操作通過分發(fā)一個 block 到它的展示隊列來執(zhí)行 savePresentedItemChangesWithCompletionHandler: 方法來完成。展示者需要保存文件并通過執(zhí)行作為參數(shù)傳入的 block 來確認通知。除了改變通知,文件展示者還用來通知應(yīng)用同步?jīng)_突。一旦一個文件的沖突版本被下載,一個新的文件版本被加入到版本存儲里。所有的展現(xiàn)者通過 presentedItemDidGainVersion: 被通知有一個新版本被創(chuàng)建。該回調(diào)接收一個引用了潛在沖突的 NSFileVersion 實例。
文件展示者還可以被用來監(jiān)視文件夾內(nèi)容。例如,一旦 iCloud 改變文件夾內(nèi)容,如創(chuàng)建,刪除或者移動文件,應(yīng)用應(yīng)該被通知到以便更新它的文檔展示。為此,應(yīng)用可以對展示的目錄注冊一個實現(xiàn)了 NSFilePresenter 協(xié)議的實例。一個目錄的文件展示者會收到任何文件夾或其中文件或子文件夾的改變的通知。比如一個文件夾內(nèi)的文件被修改,展示者會收到一個引用了該文件的 URL 的 presentedSubitemDidChangeAtURL: 通知。
因為帶寬和電池壽命在移動設(shè)備上更加有限,iOS 不會自動從 iCloud 下載新文件。而是由應(yīng)用手動決定何時來觸發(fā)下載新文件到開放性容器中。為了持續(xù)告知應(yīng)用哪些文件可用及其同步狀態(tài),iCloud 還會同步開放性容器內(nèi)的文件元信息。應(yīng)用可以通過 NSMetadataQuery 或訪問 NSURL 的開放資源屬性查詢這些元信息。無論何時應(yīng)用想要訪問一個文件,它一定會通過 NSFileManager 的 startDownloadingUbiquitousItemAtURL:error: 來觸發(fā)下載行為。
在繼續(xù)解釋如何實現(xiàn)文件協(xié)調(diào)和觀察之前,現(xiàn)在我們將深入一些過去幾年里碰到的一些常見問題。再一次的,確保你已經(jīng)閱讀并理解了 Apple iCloud companion guide。
雖然這些文件機制的描述讓它們的使用看起來簡單明了,但其實其中有很多隱藏的陷阱。這些陷阱中有些來自于底層框架的 bug。因為 iCloud 同步延伸到操作系統(tǒng)中相當(dāng)多的層面,人們只能寄希望于蘋果能夠小心的修復(fù)這些 bug。實際上,蘋果看起來寧愿廢棄壞掉的 API 而不是修復(fù)它們。
即便如此,我們的經(jīng)驗告訴我們使用 iCloud 是非常非常容易犯錯誤的。異步,協(xié)作,基于鎖特性的文件協(xié)調(diào)和文件展示互相牽連,并不容易掌握。下面,我們將介紹整合 iCloud 文檔同步時的一些主要規(guī)則,并以這種形式分享我們的經(jīng)驗。
文件展示者代價高昂。僅當(dāng)你的應(yīng)用需要立即應(yīng)對或干預(yù)文件訪問的時候,才應(yīng)該使用它。
如果你的應(yīng)用正在展示類似文檔編輯器這樣的東西給用戶,文件展示足以勝任。這時,在其他進程寫入該文件的時候也許需要鎖住編輯器,或者還需要保存未保存的改變。然而,如果只是臨時訪問并且通知也可能會被延遲處理,就不應(yīng)該使用文件展示。例如,當(dāng)創(chuàng)建文件索引或縮略圖,查看文件更改日期并使用簡單的文件協(xié)調(diào)可能會更高效。另外,如果你正展示一個字典樹的內(nèi)容,在樹的根節(jié)點注冊 一個 展示者或用 NSMetadataQuery 來延遲獲取改變通知會可能會非常高效。
是什么讓文件展示代價如此高昂?它需要很多的進程間通信:每個文件上注冊的展示者在其他進程獲取文件的訪問權(quán)時都被要求釋放該文件。比如另一個進程嘗試讀取一個文件,該文件的展示者會被要求保存所有未保存的內(nèi)容 (savePresentedItemChangesWithCompletionHandler:)。它們還會被要求釋放文件給讀取者(relinquishPresentedItemToReader:),例如文件被讀取時暫時鎖住編輯器。
這些通知每一個都需要分發(fā),加工并由各自的接收者確認。并且因為只有實現(xiàn)的進程知道哪些通知會被處理,所以即使展示者沒有實現(xiàn)任何方法,進程間也會為每一個可能的通知進行通信。
另外,每個步驟都需要在讀取進程,展示進程和文件協(xié)調(diào)守護進程 (filecoordinationd) 間的多重上下文的切換。結(jié)果就導(dǎo)致了一個簡單的文件訪問很快就變成耗費資源的操作。
除此之外,如果太多的展示者被注冊,文件協(xié)調(diào)守護進程可能會刪除重要的系統(tǒng)資源。對于每一個展示者,都需要打開并監(jiān)聽每一個它所描述的路徑上的文件夾。尤其在 OS X Lion 和 iOS 5 上,這些資源是非常稀少的,過度的使用很容易導(dǎo)致文件協(xié)調(diào)守護進程的鎖死或崩潰。
基于這些原因,我們強烈建議不要在目錄樹的每一個節(jié)點上增加文件展示者,只根據(jù)需要使用最少的文件展示者。
雖然文件協(xié)調(diào)要比文件展示節(jié)約資源,但它仍然給你的應(yīng)用和整個系統(tǒng)增加額外的負擔(dān)。
每當(dāng)你的應(yīng)用正在協(xié)調(diào)一個文件,其他同時想要訪問同一個文件的進程可能需要等待。因此你不該在協(xié)調(diào)文件時執(zhí)行過于耗時的任務(wù)。如果你這么做了,比如存儲了大文件,你可以考慮將它存儲到一個臨時文件夾,隨后在協(xié)調(diào)訪問時使用硬連接。注意每一個協(xié)調(diào)的訪問都可能會觸發(fā)另一個進程上的文件展示者 -- 該展示者可能需要時間在你的訪問之前更新文件。始終考慮使用諸如 NSFileCoordinatorReadingWithoutChanges 這樣的標識,除非需要讀取文件的最新版本。
雖然你的應(yīng)用的開放性容器可能不會被其他應(yīng)用訪問,過分的文件協(xié)調(diào)仍然可能成為 iCloud 的一個問題,執(zhí)行太多的協(xié)調(diào)請求會造成類似 ubd 的進程的資源饑餓問題。在應(yīng)用啟動階段,ubd 似乎會掃描開放性容器內(nèi)的所有文件。如果你的應(yīng)用在程序啟動階段也在執(zhí)行相同的掃描。兩個進程會經(jīng)常沖突,從而可能導(dǎo)致協(xié)調(diào)的高開銷。這時考慮更優(yōu)化的解決方案是明智的。例如掃描目錄內(nèi)容時,單獨的文件內(nèi)容訪問權(quán)限是根本不需要的。把協(xié)調(diào)工作延遲到文件內(nèi)容真正被展示的時候再進行會是不錯的選擇。
最后,絕對不要協(xié)調(diào)一個還沒有被下載的文件。文件協(xié)調(diào)會觸發(fā)對該文件的下載。不幸的是,協(xié)調(diào)將會一直等待直到下載完成,這有可能會導(dǎo)致應(yīng)用被鎖住很長一段時間。訪問一個文件之前,應(yīng)用應(yīng)該先檢查文件下載狀態(tài)。你可以通過查詢 URL 的 NSURLUbiquitousItemDownloadingStatusKey 的值或使用 NSMetadataQuery 做到這一點。
閱讀 NSFileCoordinator 的文檔,你可能注意到每個方法都有一個冗長而復(fù)雜的描述。雖然 API 文檔通常是非??煽康?,但由于同其他協(xié)調(diào)器和文件展示者交互的多樣性,以及文件夾和文件鎖的語法多樣性,都造成了很高的復(fù)雜度。有一些很容易忽略的細節(jié)和問題貫穿這些長長的描述:
NSFileCoordinatorWritingForDeleting 標識,文件展示者將無法通過 accommodatePresentedItemDeletionWithCompletionHandler: 對文件刪除操作做出影響。如果移動目錄時不使用 NSFileCoordinatorWritingForMoving,則移動操作將不會等待其子項目上正在執(zhí)行的協(xié)調(diào)操作進行完成。實現(xiàn) NSFilePresenter 的通知處理方法需要特別注意。類似 relinquishPresentedItemToReader: 這樣的通知處理方法必須被確認及告知其他進程該文件已經(jīng)對訪問準備就緒。這一般通過執(zhí)行作為參數(shù)傳入通知處理方法的確認 block 來完成。確認 block 被調(diào)用之前,其他進程不得不等待,了解這一點是尤為重要的。如果確認因為通知處理的緩慢而被延遲,協(xié)調(diào)進程也許會被擱置。如果一直沒有被執(zhí)行,則可能會永遠被掛起。
不幸的是,需要被確認的通知也會被其他完全獨立的通知拖慢。為了確保通知以正確的順序執(zhí)行,presentedItemOperationQueue 一般被設(shè)置為一個順序執(zhí)行隊列。但是一個順序隊列就意味著處理速度慢的通知會延緩隨后的通知。尤其是它們會延緩需要確認的通知,在那之前,所有的進程都將等待。
例如,假設(shè)一個 presentedItemDidChange 通知首先進入隊列。該回調(diào)漫長的處理過程將會延緩其他隨后進入隊列的通知,比如 relinquishPresentedItemToReader:。因此,該通知的確認也會被延遲,從而也導(dǎo)致等待它的進程被延緩。
綜上所述,在展示隊列里的時候 永遠不要 執(zhí)行文件協(xié)調(diào)。實際上,即使簡單的不需要任何確認的通知 (比如 presentedItemDidChange) 也會導(dǎo)致死鎖。設(shè)想兩個文件展示者同時在展示同一個文件。兩個展示者都通過執(zhí)行協(xié)調(diào)的讀取操作來處理 presentedItemDidChange 通知。如果文件發(fā)生改變,通知被發(fā)送到兩個展示者并且二者都在同一個文件上執(zhí)行協(xié)調(diào)的讀取操作。因此,兩個展示者都通過入隊一個 relinquishPresentedItemToReader: 請求對方釋放文件并等待對方確認。不幸的是,兩個展示者無法確認通知,因為它們都因為永久的等待對方確認的協(xié)調(diào)請求而阻塞了它們的展示隊列。我們在 GitHub 上提供了一個小例子展示這種死鎖。
從通知中得出正確結(jié)論并不容易。文件展示中存在的 bug 造成了有些通知處理器從未被執(zhí)行。這里初步介紹一些已知的不太規(guī)律的通知:
presentedSubitemDidChangeAtURL: 和 presentedSubitemAtURL:didMoveToURL:,所有的子項目通知要么不被調(diào)用,要么以一種難以預(yù)測的方式被調(diào)用。絕對不要依賴它們 -- 實際上,presentedSubitemDidAppearAtURL: 和 accommodatePresentedSubitemDeletionAtURL:completionHandler: 從不會被調(diào)用。NSFileCoordinatorWritingForDeleting 的文件協(xié)調(diào)來刪除文件,accommodatePresentedItemDeletionWithCompletionHandler: 才會工作。否則,你會連一個 change 的通知都收不到。itemAtURL:didMoveToURL: 時,presentedItemDidMoveToURL: 和 presentedSubitemAtURL:didMoveToURL: 才會被調(diào)用。否則項目不會收到任何有用的通知。子項目仍舊會分別針對舊的和新的 URL 收到 presentedSubitemDidChange 通知。presentedSubitemAtURL:didMoveToURL: 通知也被發(fā)送,你仍然會針對舊的和新的 URL 收到兩個額外的 presentedSubitemDidChangeAtURL: 通知。要做好準備好處理這個。一般來說,你必須注意通知可能會失效。也不應(yīng)該依賴于任何特定的通知順序。例如,當(dāng)描述一個目錄樹時,你不能期望父文件夾的通知會先于或晚于其中子項目的通知。
在文件協(xié)調(diào)和文件展示者傳遞參照著相同文件的不同的 URL 時,有幾種你需要應(yīng)對的情況。你絕不應(yīng)該使用 isEqual: 比較 URL,因為兩個不同的 URL 可能關(guān)聯(lián)同一個文件。應(yīng)該始終在比較之前標準化它們。這一點在 iOS 上尤為重要,在 iOS 中開放性容器存儲在 /var/mobile/Library/Mobile Documents/ 中,這個文件夾是 /private/var/mobile/Library/Mobile Documents/ 的符號鏈接。你會收到帶有指向同一個文件,基于 兩種路徑變體 的 URL 的展示者通知。如果你對 iCloud 和本地文檔使用文件協(xié)調(diào)代碼,這個問題在 OS X 上也會發(fā)生。
除此之外,還有幾個關(guān)于大小寫不敏感的文件系統(tǒng)的問題。如果文件系統(tǒng)要求,應(yīng)該始終確保你使用大小寫不敏感的文件名比較。文件協(xié)調(diào) block 和展示者通知可能傳遞使用不同大小寫的相同的 URL 變體。實際上,這是使用文件協(xié)調(diào)器重命名時的重要問題。為了搞懂這個問題,你需要回顧文件實際上是如何被重命名的:
[coordinator coordinateWritingItemAtURL:sourceURL
options:NSFileCoordinatorWritingForMoving
writingItemAtURL:destURL
options:0
error:NULL
byAccessor:^(NSURL *oldURL, NSURL *newURL)
{
[NSFileManager.defaultManager moveItemAtURL:oldURL toURL:newURL error:NULL];
[coordinator itemAtURL:oldURL didMoveToURL:newURL];
}];
假設(shè) sourceURL 指向一個名為 ~/Desktop/my text 的文件,destURL 使用了大寫字母的新文件名 ~/Desktop/My Text。協(xié)調(diào) block 被有意設(shè)計成傳入兩個 URL 的最新版本,以兼容等待文件訪問時發(fā)生的移動操作。現(xiàn)在,不幸的,當(dāng)改變文件名的大小寫,文件協(xié)調(diào)所執(zhí)行的 URL 校驗將會發(fā)現(xiàn)新舊兩個 URL 都存在一個有效文件,而新的 URL 是小寫 ~/Desktop/my text 的變體。訪問 block 將會接收到同樣的 小寫 URL 作為 oldURL 和 newURL,導(dǎo)致移動操作失敗。
在 iOS 中,觸發(fā)從 iCloud 的下載是應(yīng)用的責(zé)任。可以通過 NSFileManager 的 startDownloadingUbiquitousItemAtURL:error: 方法觸發(fā)下載。如果你的應(yīng)用設(shè)計成自動下載文件 (也就是不由用戶觸發(fā)),你應(yīng)該始終在一個順序后臺隊列中執(zhí)行這些下載請求。換句話說,每一個單獨的下載請求涉及到相當(dāng)多的進程間通信并可能會很耗時。另一方面,同時觸發(fā)太多的下載有時會過載 ubd 守護進程。一個普遍的錯誤就是使用 NSMetadataQuery 等待 iCloud 中的新文件然后自動觸發(fā)下載它們。因為查詢結(jié)果總是在主隊列中傳遞并且可能包含一打的更新信息,直接觸發(fā)下載會阻塞應(yīng)用很長一段時間。
為了查詢某個文件的下載或者上傳狀態(tài),你可以使用 NSURL 的資源值。在 iOS 7 / OS X 10.9 之前,一個文件的下載狀態(tài)通過 NSURLUbiquitousItemIsDownloadedKey 來確認。根據(jù)頭文件文檔,這個資源值從未正確生效過,所以在 iOS 7 和 Mavericks 中被廢棄了。現(xiàn)在蘋果建議使用 NSURLUbiquitousItemDownloadingStatusKey。在老系統(tǒng)上,你應(yīng)該使用 NSMetadataQuery 查詢 NSMetadataUbiquitousItemIsDownloadedKey 來獲得正確的下載狀態(tài)。
為你的應(yīng)用增加 iCloud 支持并不只是你增加的另一個功能,而是一個對應(yīng)用設(shè)計和實現(xiàn)有著深遠影響的決定。它既影響著你的數(shù)據(jù)模型也影響著 UI。所以不要低估支持 iCloud 所需要做出的努力。
最重要的,增加 iCloud 會引入一個新的異步層。應(yīng)用必須能夠在任何時候處理文檔和元數(shù)據(jù)的變化。這些變化上的通知可能會在不同線程上收到,這就需要在你的整個應(yīng)用中添加同步機制來對這些通知進行適當(dāng)?shù)奶幚?。你需要注意那些對于用戶文檔完整性有重大影響的關(guān)鍵代碼中的問題,比如丟失更新,競爭和死鎖等。
始終注意 iCloud 的同步保證是非常脆弱的。你只能假設(shè)文件和包是自動同步的。但你不能期望多個同時被修改的文件也會被立刻同步。比如,如果你的應(yīng)用分開存儲元信息和實際的文件的話,你一定要能夠應(yīng)對元信息會先于或晚于實際文件被下載的情況。
使用 iCloud 文檔同步同時也意味著你正在做一個發(fā)布的應(yīng)用。你的文檔會在運行著不同版本的不同設(shè)備上。你可能想要使你文件格式的不同版本向前兼容。起碼,你必須確保你的應(yīng)用在面對其他不同設(shè)備上安裝的新版本應(yīng)用創(chuàng)建的文件時不會崩潰或發(fā)生錯誤。用戶未必會立刻更新所有的設(shè)備,所以預(yù)先準備好這個問題。
最后,你的 UI 需要反映同步行為。即使這會抹殺掉一些神奇之處。尤其在 iOS 上,連接失敗和緩慢的文件轉(zhuǎn)換是現(xiàn)實狀況。你的用戶應(yīng)該被通知關(guān)于文檔的同步狀態(tài)。你應(yīng)該考慮展示文件是在被上傳還是在下載,以告知用戶他們的文檔現(xiàn)在是否可用。使用大文件時,你可能需要顯示文件傳輸進度,你的 UI 應(yīng)該優(yōu)雅一些; 如果 iCloud 不能及時給你某個文檔,你的應(yīng)用應(yīng)該響應(yīng),并且讓用戶重試或至少放棄操作。
因為涉及到多系統(tǒng)服務(wù)和外部服務(wù),調(diào)試 iCloud 問題非常困難。Xcode 5 提供的 iCloud 調(diào)試功能非常有限并且大多數(shù)時候只會告訴你 iCloud 是否已經(jīng)同步。幸運的是,還有一些差不多是官方的方法來調(diào)試 iCloud 文檔存儲。
有時你可能經(jīng)歷過 iCould 停止同步某個文件或干脆完全停止工作。實際上,這在文件協(xié)調(diào)器內(nèi)使用斷點或在一個文件操作進行期間殺掉一個進程時很容易發(fā)生。甚至如果你的應(yīng)用在某個關(guān)鍵點崩潰后也會發(fā)生。通常來說,重啟或者注銷后重新登錄 iCloud 都不能修復(fù)這個問題。
為了修復(fù)這些鎖定,一個命令行工具會非常有好處: ubcontrol。這個工具是 10.7 以后版本 OS X 的一部分。使用命令 ubcontrol -x,你能夠重置文檔同步的本地狀態(tài)。它通過重置一些私有數(shù)據(jù)庫和緩存,重啟所有涉及到的系統(tǒng)守護進程,來復(fù)原熄火的同步。同時它也會存儲一些報告分析信息到 ~/Library/Application Support/Ubiquity-backups。
雖然已經(jīng)有日志文件被寫入 ~/Library/Logs/Ubiquity 中,你也還可以通過 ubcontrol -k 7 來增加日志級別。在進行 iCloud 相關(guān)的錯誤報告時,蘋果工程師經(jīng)常會要求你這么做以便收集信息。
為了調(diào)試文件協(xié)調(diào),你還可以從文件協(xié)調(diào)守護進程中直接取回鎖狀態(tài)信息。這使你能夠得知在應(yīng)用中或多進程間可能遇到的文件協(xié)調(diào)死鎖。為了訪問這個信息,你需要在終端中執(zhí)行以下命令:
sudo heap filecoordinationd -addresses NSFileAccessArbiter
sudo lldb -n filecoordinationd
po [<address> valueForKey: @"rootNode"]
第一個命令會返回一個文件協(xié)調(diào)守護進程的內(nèi)部單例對象的地址。隨后,你關(guān)聯(lián) lldb 到運行的守護進程上。通過使用第一步取回的地址,你將會得到一個所有活動的鎖和文件展示者的狀態(tài)的概覽。調(diào)試命令會展示當(dāng)前正在被展示或協(xié)調(diào)的整個文件樹。例如,如果 TextEdit 正在展示一個名為 example.txt 的文件,你會得到以下跟蹤信息:
example.txt
<NSFileAccessNode 0x…> parent: 0x…, name: "example.txt"
presenters:
<NSFilePresenterProxy …> client: TextEdit …>
location: 0x7f9f4060b940
access claims: <none>
progress subscribers: <none>
progress publishers: <none>
children: <none>
如果你在文件協(xié)調(diào)進行時創(chuàng)建這種跟蹤 (比如通過在文件協(xié)調(diào) block 中設(shè)置斷點),你還會得到一個等待文件協(xié)調(diào)器的所有進程的列表。
如果通過 lldb 觀察文件協(xié)調(diào),你應(yīng)該始終記得盡快執(zhí)行 detach 命令。否則,全局根進程文件協(xié)調(diào)守護進程將一直等待,這會影響到系統(tǒng)中幾乎所有的應(yīng)用。
在 iOS 上,調(diào)試要更加復(fù)雜,因為你無法檢查運行的系統(tǒng)進程,你也無法使用像 ubcontrol 的命令行工具。
iCloud 鎖定在 iOS 上似乎更經(jīng)常發(fā)生。重啟應(yīng)用或設(shè)備都無效。唯一有效的修復(fù)這種問題的方法是 冷啟動。在冷啟動過程中,iOS 似乎進行了 iClouds 的內(nèi)部數(shù)據(jù)庫重置。可以通過同時按下電源鍵和 home 鍵 10 秒鐘冷啟動設(shè)備。
為了在 iOS 上激活更詳細的日志,在蘋果 developer downloads page 有一個專用的 iCloud 日志概述。如果搜索 "Bug Reporter Logging Profiles (iOS)",你將會找到一個叫做 "iCloud Logging Profile" 移動設(shè)備概述。在你的 iOS 設(shè)備上安裝該文件來激活更詳細的日志。你可以用 iTunes 同步設(shè)備來訪問這些日志.隨后,你可以在 Library/Logs/CrashReporter/Mobile Device/<Device Name>/DiagnosticLogs/Ubiquity 文件夾找到它。如果想要關(guān)掉這種加強的日志輸出,從設(shè)備刪除描述文件即可。蘋果建議你在激活或關(guān)閉概述前重啟設(shè)備。
除了在你自己的設(shè)備上調(diào)試,考慮使用蘋果服務(wù)上的調(diào)試服務(wù)可能也會有用。developer.icloud.com 上有一個特殊的 web 應(yīng)用,它允許你瀏覽存儲在開放性容器內(nèi)的所有信息和當(dāng)前傳輸狀態(tài)。
過去的幾個月,蘋果還提供了安全地在服務(wù)端對所有已連接設(shè)備進行 iCloud 重置的方法。更多信息可查看 support document。