转载

使用 iCloud API 的正确姿势

iCloud API 是一个简洁易用的库,并且给我们提供了强大的云存储能力,但他在简单易用的同时,也存在一些陷阱需要注意。这次跟大家聊聊使用 iCloud API 的时候可能遇到的一些陷阱和注意点, 看看是否有你没注意到的细节呢?

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 背后的机制。

所以呢,这里我把我看到认为重要的地方做了一个梳理。也希望能够帮助大家少走弯路,抓住重点。

原文  http://www.swiftcafe.io/2016/08/16/icloud/
正文到此结束
Loading...