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