鍍金池/ 教程/ iOS/ 精通 iCloud 文檔存儲
與四軸無人機的通訊
在沙盒中編寫腳本
結構體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學
NSString 與 Unicode
代碼簽名探析
測試
架構
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應用數(shù)據(jù)腳本化
一個完整的 Core Data 應用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡應用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應式 Android 應用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學習
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學習的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應式視圖
Square Register 中的擴張
DTrace
基礎集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設計的藝術
導航應用
線程安全類的設計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術:Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構建 iOS 應用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標簽
同步案例學習
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

精通 iCloud 文檔存儲

即便在推出 3 年后,iCloud 文檔存儲依然是一個充滿神秘、誤解和抱怨的話題。iCloud 同步經(jīng)常被批評不可靠且速度慢。雖然在 iCloud 的早期有一些嚴重的 bug,開發(fā)者們還是不得不學習有關文件同步的課程。文件同步事關重大,為應用開發(fā)帶來了新方向 -- 一個經(jīng)常被低估的方向,比如進行同步服務相關的合作時,對于處理文件異步更改的需要。

本文會介紹幾個創(chuàng)建支持 iCloud 的應用時可能會遇到的一些絆腳石。因為本文只會給出一些粗略的概述,所以如果你對 iCloud 文檔存儲還不熟悉,我們強烈建議你先閱讀 Apple iCloud companion guide。

文檔存儲簡介

iCloud 文檔存儲的核心思想非常簡單:每個應用都有至少通往一個“魔法文件夾”的入口,該文件夾可以存儲文件并且隨后在所有注冊了同一個 iCloud 帳號的設備間同步。

與其他基于文件系統(tǒng)的同步服務相比,iCloud 文檔存儲得益于與 OS X 和 iOS 的深度整合。很多系統(tǒng)框架已經(jīng)被擴展以支持 iCloud。像 NSDocumentUIDocument 這樣的類被按照可以處理外部變化來進行設計。版本存儲和 NSFileVersion 處理同步?jīng)_突。Spotlight 被用來提供同步元數(shù)據(jù),比如文件傳輸進度或者云端文檔可用性。

寫一個簡單的基于文檔并開啟了 iCloud 的 OS X 應用并不需要費多大力氣。實際上你并不需要關心任何 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="" />

然而,一旦脫離預設的路徑,你就需要了解的更多。例如,默認打開面板提供的單層文件夾以外的任何操作都需要手動完成??赡苣愕膽眯枰芾沓宋臋n內(nèi)容以外的文檔,比如像 Mail,iPhoto 或者 Ulysses (我們自己的app) 中做的那樣。這種時候,你不能依賴于 NSDocument,而需要自己實現(xiàn)它的功能。但為此你需要對 iCloud 提供的鎖和通知機制有一個深入的了解。

開發(fā)支持 iCloud 的 iOS 應用同樣需要更多的工作和知識;雖然 UIDocument 仍然管理 iCloud 文件訪問和處理同步?jīng)_突,但缺乏管理文檔和文件夾的圖形界面。因為性能和存儲空間的原因,iOS 也不會自動從云端下載新文檔。你需要使用 Spotlight 來檢索最近變化的目錄并手動觸發(fā)下載。

什么是開放性容器 (Ubiquity Container)

任何符合 App Store 條件的應用都可以使用 iCloud 文檔存儲。設置正確的授權后,就獲得了一個或多個所謂的“開放性容器”的訪問權限。這是蘋果用來稱呼“一個被 iCloud 管理和同步的目錄”的別稱。每一個開放性容器限定在一個 app id 內(nèi),由此讓每個用戶在每個應用中有一份共享的存儲倉庫。有多個應用的開發(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ā)進程訪問。首先,有一個應用呈現(xiàn)和操作容器內(nèi)的文檔。第二,有一個主要通過開放性守護 (Ubiquity Daemon ubd) 體現(xiàn)的 iCloud 架構。iCloud 架構等待應用對文檔的更改并將其上傳至蘋果云服務器。同時也等待從 iCloud 上收到的更改并相應修改容器的內(nèi)容。

由于兩個進程完全獨立于彼此工作,因此需某種形式的仲裁來避免資源競爭或丟失容器內(nèi)的文件更新的問題。應用需要使用名為 文件協(xié)調(diào) file coordination 的概念來確保對于每一個獨立文件的訪問權。該訪問權由 NSFileCoordinator 類提供。概括來說,它為每個文件提供了一個簡單的 讀-寫 鎖。這個鎖由一個通知機制擴展,該機制用于用于改善訪問同一個文件的不同進程間的合作。

這個通知機制相比于簡單的文件鎖來說是有巨大的的好處,并且提供了無縫的用戶體驗。iCloud 可能會在任何時間把文檔用一個來自其他設備的新版本覆蓋。如果一個應用當前正在顯示同一個文檔,它必須從磁盤加載新版本并向用戶展示更新過的內(nèi)容。更新過程中,應用可能需要鎖住用戶界面一段時間并隨后在此打開。甚至可能發(fā)生更壞的情況:應用可能保留著未保存的內(nèi)容,這些內(nèi)容需要首先保存到磁盤上以便檢查同步?jīng)_突。最后,在網(wǎng)絡條件良好的時候 iCloud 會上傳文件最近的版本。因此必須能夠要求應用立刻保存所有未保存的變更。

為了實現(xiàn)這個過程,文件協(xié)調(diào)伴隨著另一套名為 文件展示 (file presentation) 的機制。無論什么時候應用打開并向用戶展示一個文件,這被稱為被稱作 展示文檔,并且應該注冊一個實現(xiàn)了 NSFilePresenter 協(xié)議的對象。只要另一個進程通過一個文件協(xié)調(diào)訪問文件,文件展示者 (file presenter) 就會收到關于該文件的通知。這些通知被作為方法調(diào)用傳遞,這些方法在展示者指定的一個操作隊列(presentedItemOperationQueue)中異步執(zhí)行。

例如,在任何其他線程被允許開始一個讀取操作前,文件展示者被要求保存任何未保存的變化。這些操作通過分發(fā)一個 block 到它的展示隊列來執(zhí)行 savePresentedItemChangesWithCompletionHandler: 方法來完成。展示者需要保存文件并通過執(zhí)行作為參數(shù)傳入的 block 來確認通知。除了改變通知,文件展示者還用來通知應用同步?jīng)_突。一旦一個文件的沖突版本被下載,一個新的文件版本被加入到版本存儲里。所有的展現(xiàn)者通過 presentedItemDidGainVersion: 被通知有一個新版本被創(chuàng)建。該回調(diào)接收一個引用了潛在沖突的 NSFileVersion 實例。

文件展示者還可以被用來監(jiān)視文件夾內(nèi)容。例如,一旦 iCloud 改變文件夾內(nèi)容,如創(chuàng)建,刪除或者移動文件,應用應該被通知到以便更新它的文檔展示。為此,應用可以對展示的目錄注冊一個實現(xiàn)了 NSFilePresenter 協(xié)議的實例。一個目錄的文件展示者會收到任何文件夾或其中文件或子文件夾的改變的通知。比如一個文件夾內(nèi)的文件被修改,展示者會收到一個引用了該文件的 URL 的 presentedSubitemDidChangeAtURL: 通知。

因為帶寬和電池壽命在移動設備上更加有限,iOS 不會自動從 iCloud 下載新文件。而是由應用手動決定何時來觸發(fā)下載新文件到開放性容器中。為了持續(xù)告知應用哪些文件可用及其同步狀態(tài),iCloud 還會同步開放性容器內(nèi)的文件元信息。應用可以通過 NSMetadataQuery 或訪問 NSURL 的開放資源屬性查詢這些元信息。無論何時應用想要訪問一個文件,它一定會通過 NSFileManagerstartDownloadingUbiquitousItemAtURL:error: 來觸發(fā)下載行為。

深入 iCloud

在繼續(xù)解釋如何實現(xiàn)文件協(xié)調(diào)和觀察之前,現(xiàn)在我們將深入一些過去幾年里碰到的一些常見問題。再一次的,確保你已經(jīng)閱讀并理解了 Apple iCloud companion guide。

雖然這些文件機制的描述讓它們的使用看起來簡單明了,但其實其中有很多隱藏的陷阱。這些陷阱中有些來自于底層框架的 bug。因為 iCloud 同步延伸到操作系統(tǒng)中相當多的層面,人們只能寄希望于蘋果能夠小心的修復這些 bug。實際上,蘋果看起來寧愿廢棄壞掉的 API 而不是修復它們。

即便如此,我們的經(jīng)驗告訴我們使用 iCloud 是非常非常容易犯錯誤的。異步,協(xié)作,基于鎖特性的文件協(xié)調(diào)和文件展示互相牽連,并不容易掌握。下面,我們將介紹整合 iCloud 文檔同步時的一些主要規(guī)則,并以這種形式分享我們的經(jīng)驗。

只在需要時使用 Presenters

文件展示者代價高昂。僅當你的應用需要立即應對或干預文件訪問的時候,才應該使用它。

如果你的應用正在展示類似文檔編輯器這樣的東西給用戶,文件展示足以勝任。這時,在其他進程寫入該文件的時候也許需要鎖住編輯器,或者還需要保存未保存的改變。然而,如果只是臨時訪問并且通知也可能會被延遲處理,就不應該使用文件展示。例如,當創(chuàng)建文件索引或縮略圖,查看文件更改日期并使用簡單的文件協(xié)調(diào)可能會更高效。另外,如果你正展示一個字典樹的內(nèi)容,在樹的根節(jié)點注冊 一個 展示者或用 NSMetadataQuery 來延遲獲取改變通知會可能會非常高效。

是什么讓文件展示代價如此高昂?它需要很多的進程間通信:每個文件上注冊的展示者在其他進程獲取文件的訪問權時都被要求釋放該文件。比如另一個進程嘗試讀取一個文件,該文件的展示者會被要求保存所有未保存的內(nèi)容 (savePresentedItemChangesWithCompletionHandler:)。它們還會被要求釋放文件給讀取者(relinquishPresentedItemToReader:),例如文件被讀取時暫時鎖住編輯器。

這些通知每一個都需要分發(fā),加工并由各自的接收者確認。并且因為只有實現(xiàn)的進程知道哪些通知會被處理,所以即使展示者沒有實現(xiàn)任何方法,進程間也會為每一個可能的通知進行通信。

另外,每個步驟都需要在讀取進程,展示進程和文件協(xié)調(diào)守護進程 (filecoordinationd) 間的多重上下文的切換。結果就導致了一個簡單的文件訪問很快就變成耗費資源的操作。

除此之外,如果太多的展示者被注冊,文件協(xié)調(diào)守護進程可能會刪除重要的系統(tǒng)資源。對于每一個展示者,都需要打開并監(jiān)聽每一個它所描述的路徑上的文件夾。尤其在 OS X Lion 和 iOS 5 上,這些資源是非常稀少的,過度的使用很容易導致文件協(xié)調(diào)守護進程的鎖死或崩潰。

基于這些原因,我們強烈建議不要在目錄樹的每一個節(jié)點上增加文件展示者,只根據(jù)需要使用最少的文件展示者。

只在需要時使用協(xié)調(diào)

雖然文件協(xié)調(diào)要比文件展示節(jié)約資源,但它仍然給你的應用和整個系統(tǒng)增加額外的負擔。

每當你的應用正在協(xié)調(diào)一個文件,其他同時想要訪問同一個文件的進程可能需要等待。因此你不該在協(xié)調(diào)文件時執(zhí)行過于耗時的任務。如果你這么做了,比如存儲了大文件,你可以考慮將它存儲到一個臨時文件夾,隨后在協(xié)調(diào)訪問時使用硬連接。注意每一個協(xié)調(diào)的訪問都可能會觸發(fā)另一個進程上的文件展示者 -- 該展示者可能需要時間在你的訪問之前更新文件。始終考慮使用諸如 NSFileCoordinatorReadingWithoutChanges 這樣的標識,除非需要讀取文件的最新版本。

雖然你的應用的開放性容器可能不會被其他應用訪問,過分的文件協(xié)調(diào)仍然可能成為 iCloud 的一個問題,執(zhí)行太多的協(xié)調(diào)請求會造成類似 ubd 的進程的資源饑餓問題。在應用啟動階段,ubd 似乎會掃描開放性容器內(nèi)的所有文件。如果你的應用在程序啟動階段也在執(zhí)行相同的掃描。兩個進程會經(jīng)常沖突,從而可能導致協(xié)調(diào)的高開銷。這時考慮更優(yōu)化的解決方案是明智的。例如掃描目錄內(nèi)容時,單獨的文件內(nèi)容訪問權限是根本不需要的。把協(xié)調(diào)工作延遲到文件內(nèi)容真正被展示的時候再進行會是不錯的選擇。

最后,絕對不要協(xié)調(diào)一個還沒有被下載的文件。文件協(xié)調(diào)會觸發(fā)對該文件的下載。不幸的是,協(xié)調(diào)將會一直等待直到下載完成,這有可能會導致應用被鎖住很長一段時間。訪問一個文件之前,應用應該先檢查文件下載狀態(tài)。你可以通過查詢 URL 的 NSURLUbiquitousItemDownloadingStatusKey 的值或使用 NSMetadataQuery 做到這一點。

協(xié)調(diào)方法的幾個備注

閱讀 NSFileCoordinator 的文檔,你可能注意到每個方法都有一個冗長而復雜的描述。雖然 API 文檔通常是非常可靠的,但由于同其他協(xié)調(diào)器和文件展示者交互的多樣性,以及文件夾和文件鎖的語法多樣性,都造成了很高的復雜度。有一些很容易忽略的細節(jié)和問題貫穿這些長長的描述:

  1. 認真選擇協(xié)調(diào)選項。它們真的對文件協(xié)調(diào)器和文件展示者有著影響。比如,如果沒有采用 NSFileCoordinatorWritingForDeleting 標識,文件展示者將無法通過 accommodatePresentedItemDeletionWithCompletionHandler: 對文件刪除操作做出影響。如果移動目錄時不使用 NSFileCoordinatorWritingForMoving,則移動操作將不會等待其子項目上正在執(zhí)行的協(xié)調(diào)操作進行完成。
  2. 始終認為協(xié)調(diào)調(diào)用可能會失敗并返回錯誤。因為文件協(xié)調(diào)同 iCloud 交互,如果被協(xié)調(diào)的文件不能被下載,協(xié)調(diào)調(diào)用會失敗并產(chǎn)生一條錯誤信息,并且你實際的文件操作可能不會被執(zhí)行。如果沒有正確的實現(xiàn)錯誤處理方法,你的應用可能不會注意到這樣的問題。
  3. 在進入?yún)f(xié)調(diào) block 之后檢查文件狀態(tài)。協(xié)調(diào)請求之后,也許很長時間已經(jīng)過去了。這時,應用操作文件的前提條件可能已經(jīng)失效。你想寫入的信息直到重新獲得鎖之前有可能都是臟數(shù)據(jù)。也可能在你等待獲得寫入權限的時候文件已經(jīng)被刪除。這時你可能會無意中再次創(chuàng)建已經(jīng)被刪除的文件。

通知死鎖

實現(xiàn) NSFilePresenter 的通知處理方法需要特別注意。類似 relinquishPresentedItemToReader: 這樣的通知處理方法必須被確認及告知其他進程該文件已經(jīng)對訪問準備就緒。這一般通過執(zhí)行作為參數(shù)傳入通知處理方法的確認 block 來完成。確認 block 被調(diào)用之前,其他進程不得不等待,了解這一點是尤為重要的。如果確認因為通知處理的緩慢而被延遲,協(xié)調(diào)進程也許會被擱置。如果一直沒有被執(zhí)行,則可能會永遠被掛起。

不幸的是,需要被確認的通知也會被其他完全獨立的通知拖慢。為了確保通知以正確的順序執(zhí)行,presentedItemOperationQueue 一般被設置為一個順序執(zhí)行隊列。但是一個順序隊列就意味著處理速度慢的通知會延緩隨后的通知。尤其是它們會延緩需要確認的通知,在那之前,所有的進程都將等待。

例如,假設一個 presentedItemDidChange 通知首先進入隊列。該回調(diào)漫長的處理過程將會延緩其他隨后進入隊列的通知,比如 relinquishPresentedItemToReader:。因此,該通知的確認也會被延遲,從而也導致等待它的進程被延緩。

綜上所述,在展示隊列里的時候 永遠不要 執(zhí)行文件協(xié)調(diào)。實際上,即使簡單的不需要任何確認的通知 (比如 presentedItemDidChange) 也會導致死鎖。設想兩個文件展示者同時在展示同一個文件。兩個展示者都通過執(zhí)行協(xié)調(diào)的讀取操作來處理 presentedItemDidChange 通知。如果文件發(fā)生改變,通知被發(fā)送到兩個展示者并且二者都在同一個文件上執(zhí)行協(xié)調(diào)的讀取操作。因此,兩個展示者都通過入隊一個 relinquishPresentedItemToReader: 請求對方釋放文件并等待對方確認。不幸的是,兩個展示者無法確認通知,因為它們都因為永久的等待對方確認的協(xié)調(diào)請求而阻塞了它們的展示隊列。我們在 GitHub 上提供了一個小例子展示這種死鎖。

通知缺陷

從通知中得出正確結論并不容易。文件展示中存在的 bug 造成了有些通知處理器從未被執(zhí)行。這里初步介紹一些已知的不太規(guī)律的通知:

  1. 除了 presentedSubitemDidChangeAtURL:presentedSubitemAtURL:didMoveToURL:,所有的子項目通知要么不被調(diào)用,要么以一種難以預測的方式被調(diào)用。絕對不要依賴它們 -- 實際上,presentedSubitemDidAppearAtURL:accommodatePresentedSubitemDeletionAtURL:completionHandler: 從不會被調(diào)用。
  2. 只有通過使用了 NSFileCoordinatorWritingForDeleting 的文件協(xié)調(diào)來刪除文件,accommodatePresentedItemDeletionWithCompletionHandler: 才會工作。否則,你會連一個 change 的通知都收不到。
  3. 只有文件展示者執(zhí)行 itemAtURL:didMoveToURL: 時,presentedItemDidMoveToURL:presentedSubitemAtURL:didMoveToURL: 才會被調(diào)用。否則項目不會收到任何有用的通知。子項目仍舊會分別針對舊的和新的 URL 收到 presentedSubitemDidChange 通知。
  4. 即使文件被正確移動,presentedSubitemAtURL:didMoveToURL: 通知也被發(fā)送,你仍然會針對舊的和新的 URL 收到兩個額外的 presentedSubitemDidChangeAtURL: 通知。要做好準備好處理這個。

一般來說,你必須注意通知可能會失效。也不應該依賴于任何特定的通知順序。例如,當描述一個目錄樹時,你不能期望父文件夾的通知會先于或晚于其中子項目的通知。

注意 URL 變化

在文件協(xié)調(diào)和文件展示者傳遞參照著相同文件的不同的 URL 時,有幾種你需要應對的情況。你絕不應該使用 isEqual: 比較 URL,因為兩個不同的 URL 可能關聯(lián)同一個文件。應該始終在比較之前標準化它們。這一點在 iOS 上尤為重要,在 iOS 中開放性容器存儲在 /var/mobile/Library/Mobile Documents/ 中,這個文件夾是 /private/var/mobile/Library/Mobile Documents/ 的符號鏈接。你會收到帶有指向同一個文件,基于 兩種路徑變體 的 URL 的展示者通知。如果你對 iCloud 和本地文檔使用文件協(xié)調(diào)代碼,這個問題在 OS X 上也會發(fā)生。

除此之外,還有幾個關于大小寫不敏感的文件系統(tǒng)的問題。如果文件系統(tǒ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];
}];

假設 sourceURL 指向一個名為 ~/Desktop/my text 的文件,destURL 使用了大寫字母的新文件名 ~/Desktop/My Text。協(xié)調(diào) block 被有意設計成傳入兩個 URL 的最新版本,以兼容等待文件訪問時發(fā)生的移動操作?,F(xiàn)在,不幸的,當改變文件名的大小寫,文件協(xié)調(diào)所執(zhí)行的 URL 校驗將會發(fā)現(xiàn)新舊兩個 URL 都存在一個有效文件,而新的 URL 是小寫 ~/Desktop/my text 的變體。訪問 block 將會接收到同樣的 小寫 URL 作為 oldURLnewURL,導致移動操作失敗。

請求下載

在 iOS 中,觸發(fā)從 iCloud 的下載是應用的責任??梢酝ㄟ^ NSFileManagerstartDownloadingUbiquitousItemAtURL:error: 方法觸發(fā)下載。如果你的應用設計成自動下載文件 (也就是不由用戶觸發(fā)),你應該始終在一個順序后臺隊列中執(zhí)行這些下載請求。換句話說,每一個單獨的下載請求涉及到相當多的進程間通信并可能會很耗時。另一方面,同時觸發(fā)太多的下載有時會過載 ubd 守護進程。一個普遍的錯誤就是使用 NSMetadataQuery 等待 iCloud 中的新文件然后自動觸發(fā)下載它們。因為查詢結果總是在主隊列中傳遞并且可能包含一打的更新信息,直接觸發(fā)下載會阻塞應用很長一段時間。

為了查詢某個文件的下載或者上傳狀態(tài),你可以使用 NSURL 的資源值。在 iOS 7 / OS X 10.9 之前,一個文件的下載狀態(tài)通過 NSURLUbiquitousItemIsDownloadedKey 來確認。根據(jù)頭文件文檔,這個資源值從未正確生效過,所以在 iOS 7 和 Mavericks 中被廢棄了。現(xiàn)在蘋果建議使用 NSURLUbiquitousItemDownloadingStatusKey。在老系統(tǒng)上,你應該使用 NSMetadataQuery 查詢 NSMetadataUbiquitousItemIsDownloadedKey 來獲得正確的下載狀態(tài)。

綜合考慮

為你的應用增加 iCloud 支持并不只是你增加的另一個功能,而是一個對應用設計和實現(xiàn)有著深遠影響的決定。它既影響著你的數(shù)據(jù)模型也影響著 UI。所以不要低估支持 iCloud 所需要做出的努力。

最重要的,增加 iCloud 會引入一個新的異步層。應用必須能夠在任何時候處理文檔和元數(shù)據(jù)的變化。這些變化上的通知可能會在不同線程上收到,這就需要在你的整個應用中添加同步機制來對這些通知進行適當?shù)奶幚?。你需要注意那些對于用戶文檔完整性有重大影響的關鍵代碼中的問題,比如丟失更新,競爭和死鎖等。

始終注意 iCloud 的同步保證是非常脆弱的。你只能假設文件和包是自動同步的。但你不能期望多個同時被修改的文件也會被立刻同步。比如,如果你的應用分開存儲元信息和實際的文件的話,你一定要能夠應對元信息會先于或晚于實際文件被下載的情況。

使用 iCloud 文檔同步同時也意味著你正在做一個發(fā)布的應用。你的文檔會在運行著不同版本的不同設備上。你可能想要使你文件格式的不同版本向前兼容。起碼,你必須確保你的應用在面對其他不同設備上安裝的新版本應用創(chuàng)建的文件時不會崩潰或發(fā)生錯誤。用戶未必會立刻更新所有的設備,所以預先準備好這個問題。

最后,你的 UI 需要反映同步行為。即使這會抹殺掉一些神奇之處。尤其在 iOS 上,連接失敗和緩慢的文件轉(zhuǎn)換是現(xiàn)實狀況。你的用戶應該被通知關于文檔的同步狀態(tài)。你應該考慮展示文件是在被上傳還是在下載,以告知用戶他們的文檔現(xiàn)在是否可用。使用大文件時,你可能需要顯示文件傳輸進度,你的 UI 應該優(yōu)雅一些; 如果 iCloud 不能及時給你某個文檔,你的應用應該響應,并且讓用戶重試或至少放棄操作。

調(diào)試

因為涉及到多系統(tǒng)服務和外部服務,調(diào)試 iCloud 問題非常困難。Xcode 5 提供的 iCloud 調(diào)試功能非常有限并且大多數(shù)時候只會告訴你 iCloud 是否已經(jīng)同步。幸運的是,還有一些差不多是官方的方法來調(diào)試 iCloud 文檔存儲。

在 OS X 上調(diào)試

有時你可能經(jīng)歷過 iCould 停止同步某個文件或干脆完全停止工作。實際上,這在文件協(xié)調(diào)器內(nèi)使用斷點或在一個文件操作進行期間殺掉一個進程時很容易發(fā)生。甚至如果你的應用在某個關鍵點崩潰后也會發(fā)生。通常來說,重啟或者注銷后重新登錄 iCloud 都不能修復這個問題。

為了修復這些鎖定,一個命令行工具會非常有好處: ubcontrol。這個工具是 10.7 以后版本 OS X 的一部分。使用命令 ubcontrol -x,你能夠重置文檔同步的本地狀態(tài)。它通過重置一些私有數(shù)據(jù)庫和緩存,重啟所有涉及到的系統(tǒng)守護進程,來復原熄火的同步。同時它也會存儲一些報告分析信息到 ~/Library/Application Support/Ubiquity-backups

雖然已經(jīng)有日志文件被寫入 ~/Library/Logs/Ubiquity 中,你也還可以通過 ubcontrol -k 7 來增加日志級別。在進行 iCloud 相關的錯誤報告時,蘋果工程師經(jīng)常會要求你這么做以便收集信息。

為了調(diào)試文件協(xié)調(diào),你還可以從文件協(xié)調(diào)守護進程中直接取回鎖狀態(tài)信息。這使你能夠得知在應用中或多進程間可能遇到的文件協(xié)調(diào)死鎖。為了訪問這個信息,你需要在終端中執(zhí)行以下命令:

sudo heap filecoordinationd -addresses NSFileAccessArbiter
sudo lldb -n filecoordinationd
po [<address> valueForKey: @"rootNode"]

第一個命令會返回一個文件協(xié)調(diào)守護進程的內(nèi)部單例對象的地址。隨后,你關聯(lián) lldb 到運行的守護進程上。通過使用第一步取回的地址,你將會得到一個所有活動的鎖和文件展示者的狀態(tài)的概覽。調(diào)試命令會展示當前正在被展示或協(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 中設置斷點),你還會得到一個等待文件協(xié)調(diào)器的所有進程的列表。

如果通過 lldb 觀察文件協(xié)調(diào),你應該始終記得盡快執(zhí)行 detach 命令。否則,全局根進程文件協(xié)調(diào)守護進程將一直等待,這會影響到系統(tǒng)中幾乎所有的應用。

在 iOS 上調(diào)試

在 iOS 上,調(diào)試要更加復雜,因為你無法檢查運行的系統(tǒng)進程,你也無法使用像 ubcontrol 的命令行工具。

iCloud 鎖定在 iOS 上似乎更經(jīng)常發(fā)生。重啟應用或設備都無效。唯一有效的修復這種問題的方法是 冷啟動。在冷啟動過程中,iOS 似乎進行了 iClouds 的內(nèi)部數(shù)據(jù)庫重置。可以通過同時按下電源鍵和 home 鍵 10 秒鐘冷啟動設備。

為了在 iOS 上激活更詳細的日志,在蘋果 developer downloads page 有一個專用的 iCloud 日志概述。如果搜索 "Bug Reporter Logging Profiles (iOS)",你將會找到一個叫做 "iCloud Logging Profile" 移動設備概述。在你的 iOS 設備上安裝該文件來激活更詳細的日志。你可以用 iTunes 同步設備來訪問這些日志.隨后,你可以在 Library/Logs/CrashReporter/Mobile Device/<Device Name>/DiagnosticLogs/Ubiquity 文件夾找到它。如果想要關掉這種加強的日志輸出,從設備刪除描述文件即可。蘋果建議你在激活或關閉概述前重啟設備。

在 iCloud Servers 上調(diào)試

除了在你自己的設備上調(diào)試,考慮使用蘋果服務上的調(diào)試服務可能也會有用。developer.icloud.com 上有一個特殊的 web 應用,它允許你瀏覽存儲在開放性容器內(nèi)的所有信息和當前傳輸狀態(tài)。

過去的幾個月,蘋果還提供了安全地在服務端對所有已連接設備進行 iCloud 重置的方法。更多信息可查看 support document。