轉自:SwiftCafe
iCloud 自從隨著 iOS 5 推出以來, 經過幾次迭代變得越來越完善。 如果你的 App 需要用到文檔存儲相關的功能,那麼 iCloud Document API 是一個很不錯的選擇。 相比於其他雲存儲平台,iCloud 和 iOS 設備高度集成,並且 API 的使用更加便捷。 但同時,它的 API 也存在很多雷區,需要我們格外注意。 我們就來看看使用 iCloud API 的正確姿勢吧。
正確姿勢一 - 檢測 iCloud 可用性
在使用 iCloud Document 之前,我們先要檢測當前設備是否開啟了 iCloud 功能,如果設備本身沒有開啟 iCloud,我們後續的操作就都會失敗。 通過 NSFileManager 來獲取 iCloud 的狀態:
NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
這個方法接受一個參數, 就是要獲取的容器標識。 所謂容器標識, 大多數應用只會用到一個 iCloud 容器,所以我們這裡傳入 nil, 就代表默認獲取第一個可用的容器。
接下來,這個方法內部會查找當前應用擁有的 iCloud 容器, 如果找到就會返回這個容器的 URL, 證明當前應用的 iCloud 容器可用。 如果找不到,就會返回 nil, 證明當前應用的 iCloud 不可用。
這樣我們就能根據這個方法的返回值來片段當前設備開啟了 iCloud 服務。 只有在服務開啟的時候,後續的操作才能進行。
這個方法獲取的只是 iCloud 容器的根目錄 URL, 我們大多數情況是不使用根目錄的, 我們應該使用 Documents 目錄, 所以這個方法還需要修改一下:
func getiCloudDocumentURL() -> NSURL? { if let url = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil) { return url.URLByAppendingPathComponent("Documents") } return nil }
這樣, 判斷 iCloud 可用性以及獲取目錄 URL 的邏輯就都完成啦。 在實現具體邏輯的時候, 使用這個方法獲取 URL, 如果能夠獲取,就可以進行下一步的文件列表操作了。 如果獲取失敗,就表示當前設備的 iCloud 服務不可用,或者當前 App 的 iCloud 服務沒有開啟, 這時候可以給用戶一個提示, 去設置 iCloud。
最後一個小 Tip, iCloud 容器和你 App 文件沙盒, 在 iOS 文件系統中其實是分別存放在兩個不同的地方的:
iCloud 文件路徑格式 file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~xxx~aaa/Documents
App 沙盒文件路徑格式 file:///var/mobile/Containers/Data/Application/3B4376B3-89B5-3342-8057-3450D4224518/Documents/
由此可見, 這也是為什麼 iCloud 和 Sandbox 文件路徑訪問需要兩套不同的方式的原因了。
正確姿勢二 - 獲取 iCloud 文件列表
iCloud 的另外一個陷阱就是文件列表的獲取。 如果你有過 iOS 開發經驗, 那麼當得到了一個目錄 URL 的時候, 你可能會想到這樣得到目錄中的文件列表:
if let documentURL = getiCloudDocumentURL() { NSFileManager.defaultManager().contentsOfDirectoryAtURL(documentURL, includingPropertiesForKeys: nil, options: NSDirectoryEnumerationOptions.SkipsHiddenFiles) }
從代碼上看起來似乎沒什麼問題, 但如果你將這段代碼用到 iCloud 文件的操作上, 很快你就會發現問題了。
這還要從 iCloud 在 iOS 系統上的運作機制說起。 其實 iCloud 的所有文件同步操作都是用過駐留在系統的一個進程進行的。 也就是說你的 App 所對應的 iCloud 目錄,除了你的 App 進程會操作它, iCloud Daemon 也會操作它。 這就會帶來並發訪問資源的管理問題。
但這還不是全部,還有一個更好玩兒的。 假如你現在是用是 Mac 筆記本,那麼其他設備只要向 iCloud 容器中添加新的文件,你的 iCloud Daemon 進程就會自動的將它們下載下來。
但在 iOS 系統中, iCloud Daemon 因為手機耗電以及網絡流量等考慮, 是不會自動下載其他設備新添加到容器中的文件的。 只有你請求打開某個文件的時候才會去下載它的內容。
相信經過我這麼一說,大家就察覺到問題了, 如果使用上面那種遍歷目錄的方法。 對於那些從其他設備添加,並且還沒有下載到本地的文件,就會遍歷不到了。很顯然, 這不是我們期望的結果。
那麼在 iOS 上面, 我們怎麼取得完整的文件列表呢? iCloud 在 iOS 上雖然不會自動下載這些新添加的文件,但會將這些新文件的元信息(MetaData)傳輸過來,比如文件名,文件尺寸,修改時間等等。也就是說我們需要查詢文件元信息的列表,就可以得到和服務端同步的文件列表了。
綜上所述, 獲取 iCloud 文件列表的正確姿勢是這樣:
let metaQuery = NSMetadataQuery() func listFile() { metaQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] metaQuery.predicate = NSPredicate(value: true) NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(listReceived), name:NSMetadataQueryDidFinishGatheringNotification, object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(listReceived), name:NSMetadataQueryDidUpdateNotification, object: nil) metaQuery.startQuery() } func listReceived() { let results = metaQuery.results for item in results { let fileURL = item.valueForAttribute(NSMetadataItemURLKey) } NSNotificationCenter.defaultCenter().removeObserver(self, name: NSMetadataQueryDidFinishGatheringNotification, object: nil) NSNotificationCenter.defaultCenter().removeObserver(self, name: NSMetadataQueryDidUpdateNotification, object: nil) metaQuery.stopQuery() }
這段代碼篇幅稍長, 首先我們初始化了一個 NSMetadataQuery 實例, 然後在 listFile 方法中設置它的屬性。
metaQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] 這個屬性表示我們要查詢 iCloud 的 Documents 目錄中的文件列表。
metaQuery.predicate = NSPredicate(value: true) 這個是對結果集的過濾選項, 我們這個 Query 默認接受所有文件。
NSMetadataQueryDidUpdateNotification 和 NSMetadataQueryDidFinishGatheringNotification 這兩個通知分別表示得到查詢數據的更新,以及得到全部查詢數據。
接下來 listReceived 方法處理這兩個通知, 這時候可以去到 metaQuery 的 results 屬性,代表我們查找到的文件元信息列表, 最後使用 item.valueForAttribute(NSMetadataItemURLKey) 這樣方法就可以得到包括文件 URL,文件尺寸,修改時間這些信息了。
在獲取完相關的信息後, 我們可以調用 metaQuery.stopQuery() 方法結束查詢操作。 並且 NSMetadataQuery 除了提供我們剛才這種一次性查詢之外,還提供一個長期駐留查詢的機制, 只要它的查詢條件所覆蓋的內容發生了變化,就會發送通知給我們。
但要注意一點, NSMetadataQuery 查詢操作只能在我們 App 進入前台的時候開啟, 也就是說當我們的 App 切換到後台的時候, 要記得暫停查詢操作。
正確姿勢三 - 使用 UIDocument
之前的文章中,我們討論過一次關於 UIDocument 的內容,大家可以點擊這裡 回顧一下。
對於 iCloud 相關的文件操作,最好要使用 UIDocument 來進行。
為什麼要使用 UIDocument 而不是直接通過文件操作 API 來進行呢? 這要從咱們剛才說到的進程間資源共享說起。
首先切記一點, iCloud 容器中的文件不止你的 App 在操作它。 還有另外一個叫做 iCloud Daemon 的家伙也在操作它。
這種多個進程共同操作一個資源的時候,就需要保證在同一時刻只有一個進程會操作這個資源。 如果兩個進程同時操作這個資源,就會造成非常危險的後果。
比如你的 App 正在把你剛剛修改的內容寫入一個文件, 而這個時候你的 iCloud Daemon 有可能將服務端對這個文件的改動也寫入進來。 這樣,你們最終的結果肯定會是其中一個操作覆蓋了另一個操作。
這就需要一個同步機制, 當你的 App 進程在進行寫入操作的時候, iCloud Daemon 會進行等待,當你寫入完成後, 它才會將服務端的改動也同步過來。
當然了,上面這個簡單例子只是為了讓大家對資源的安全訪問有一個直觀的理解,在實際的情況要比我描述的這種更加復雜。
回到我們開始的討論,類似 NSFileManager 這樣的 API,是不能夠保證多個進程之間這種安全訪問機制的。 所以 iOS 引入了兩個類 NSFileCoordinator 和 NSFilePresenter。
給大家一個直觀的描述,假設有4個人同時操作一個文檔, 每個人都會得到一個 NSFileCoordinator 和 NSFilePresenter。 假設其中一個人要給這個文檔中加兩行字,他先要用他自己的 NSFileCoordinator 發出通知給其他三個人。
其他 3 個人的 NSFilePresenter 會接收到這個通知,每個在這個時候都可以通過 NSFilePresenter 進行一些准備工作,當這些准備工作完成後,繼續通過 NSFilePresenter 告訴通知的發起方,准備完成。
只要這三個人都發出了准備完成的通知後, 第一個發起者才能把這兩行字寫上去。
描述的比較直接~ 這也就是 NSFileCoordinator 和 NSFilePresenter 的基本原理,通過這個方式保證文件在多個進程鍵的訪問安全。 關於這兩個類的實際操作還會更復雜些,咱們在這裡先做一個簡要的了解。
iCloud 的官方文檔中其實是強制要求使用者對文件的操作都通過 NSFileCoordinator 和 NSFilePresenter 來進行的。
但文件操作的邏輯其實很多, 而且這兩個類的使用其實相對復雜, 如果不熟悉用錯的話可能還會造成調試困難。所以基於這些原因,UIDocument 才浮出水面。這也是 UIDocument 最重要的好處。它的內部已經對 NSFileCoordinator 和 NSFilePresenter 做了封裝,我們直接使用就好。
我們通過文件的 URL 即可初始化 UIDocument:
let document = UIDocument(fileURL: fileURL)
初始化完成後, 我們直接打開即可:
document.openWithCompletionHandler { success in document.contents }
UIDocument 會區分 Sanbox 和 iCloud 進行相應的處理, 並且處理多進程操作的問題。 調用完 openWithCompletionHandler 之後, 我們的 UIDocument 相當於已經打開的 NSFilePresenter, 如果其他進程要修改這個文件,我們就會接到通知,並進行准備工作, UIDocument 已經給我們提供了默認的實現。
當文檔使用完畢後,可以調用:
document.closeWithCompletionHandler { success in }
這個方法除了關閉文檔之外,還會自動為我們處理文件保存操作,以及釋放 NSFilePresenter 的占用。
最後, UIDocument 我們不能夠直接使用, 還需要實現兩個方法:
class Doc : UIDocument { var fileContents: String = ""; override func contentsForType(typeName: String) throws -> AnyObject { return fileContents.dataUsingEncoding(NSUTF8StringEncoding)! } override func loadFromContents(contents: AnyObject, ofType typeName: String?) throws { fileContents = NSString(data: contents as! NSData, encoding: NSUTF8StringEncoding) as! String } }
UIDocument 雖然為我們實現了很多底層操作, 但如何獲取文件內容的邏輯,還是留給了我們自己來實現。 contentsForType 和 loadFromContents 都是回調方法。 contentsForType 方法用於保存文件時提供給 UIDocument 要保存的數據, loadFromContents 用於 UIDocument 成功打開文件後,我們將數據解析成我們需要的文件內容,然後再保存起來。
之所以這樣做, 我理解應該是 UIDocument 只是對通用文件的一個抽象。 可以是普通文本文件, 但也可以是其他類型的文件格式, 所以它傳遞給我們的就是一個原始的 data 數據,如何解析和處理這個數據,就交給了我們自己。
這裡對 UIDocument 的基本使用給大家做了一個介紹, 更詳細的使用方法大家就需要參考相關文檔了。
結尾
自己也曾經開發過 iCloud 相關的功能, 剛開始總會莫名其妙的陷入一些陷阱當中, 出現莫名其妙的錯誤。 於是呢,花了幾天時間好好研究了一下 iCloud 相關的文檔。 在過程中發現 iCloud 的文檔分布非常多, 從設計規范,到基於文檔的編程規范,等等。散步在很多個主題文檔中。所以只有把他們全都融匯起來才能慢慢的理解這套 API 背後的機制。
所以呢,這裡我把我看到認為重要的地方做了一個梳理。也希望能夠幫助大家少走彎路,抓住重點。