NSFileCoordinator 是 iOS 和 macOS 中的一个 Foundation 类,可在多个线程、进程或扩展同时工作时提供安全的文件访问。根据 Apple Developer Documentation, 2024,NSFileCoordinator 可在读取和写入文件时防止竞态条件 (race conditions),确保没有进程在另一个进程修改数据时读取数据。该协调器用于 iCloud Drive、File Provider Extension 以及任何多线程文件操作。
要点
NSFileCoordinator — 是 Apple 在 iOS 5 和 macOS 10.7 Lion 中引入的操作系统级文件访问同步机制。与传统的锁 (NSLock、pthread_mutex) 不同,协调器在文件系统级别工作,可以协调不同进程之间的访问,而不仅仅是同一应用程序的线程之间。
对 NSFileCoordinator 的需求源自 iOS 中的 Sandbox 架构:每个进程(应用程序、扩展、系统服务)在隔离的环境中运行,拥有自己的文件访问权限。当多个进程试图同时读取和写入同一文件时(例如在 iCloud Drive 同步期间),如果没有协调器,就会产生竞态条件:进程 A 在进程 B 已部分覆盖文件时读取该文件。
根据 WWDC 2023,Apple 强烈建议在 Ubiquity container (iCloud Drive) 中的所有文件操作以及使用 File Provider Extension 时使用 NSFileCoordinator。忽略协调是 iOS 应用程序中数据损坏和不可重现错误的常见原因之一。
协调意图 (NSFileCoordinator.ReadingIntent / WritingIntent) — 是一个声明线程或进程计划执行的操作类型的对象。协调器使用这些意图来确定访问顺序和解决冲突。
| 意图类型 | 描述 | 何时使用 |
|---|---|---|
| ReadingIntent | 读取文件而不修改 | 打开文档,加载数据 |
| WritingIntent | 写入并可能修改内容 | 保存文档,编辑 |
| ReadingIntent(URL, options: .withoutChanges) | 读取而不跟踪更改 | 快速预览内容 |
| WritingIntent(URL, options: .contentIndependentMetadataOnly) | 仅修改元数据 | 更新日期或属性 |
| WritingIntent(URL, options: .forDeleting) | 删除文件 | 用户删除文档 |
协调规则:允许多个同时读取(如果没有活动写入),写入是独占的——写入操作期间不允许任何读取或写入。这符合 readers-writer 锁模型,但通过 launchd 和 XPC 提供额外的进程间协调支持。
重要细微差别: NSFileCoordinator 不会阻止通过普通 NSData 或 FileManager 访问文件——它只协调显式包装在协调块中的操作。如果另一个线程绕过协调器直接访问文件,就会产生协调器本应防止的竞态条件。
基本模式 使用 NSFileCoordinator 包含三个步骤:创建协调器实例、声明意图(读取或写入)以及在协调块内执行操作。协调器确保没有其他协调器同时处理同一文件。
import Foundation
let coordinator = NSFileCoordinator()
let fileURL = getDocumentURL()
// 安全读取
let readIntent = NSFileCoordinator
.ReadingIntent(url: fileURL)
var content: Data?
var readError: NSError?
coordinator.coordinate(with: readIntent) { error in
if let error = error {
readError = error
return
}
content = try? Data(contentsOf: fileURL)
}
// 安全写入
let writeIntent = NSFileCoordinator
.WritingIntent(url: fileURL)
coordinator.coordinate(with: writeIntent) { error in
guard error == nil else { return }
do {
try newData.write(to: fileURL)
} catch {
Logger.storage.error(
"Write failed: \(error)"
)
}
}
批量操作 — 协调器可以使用意图数组在单个操作中处理多个文件。这对于将一组文件作为单个事务移动、复制或删除非常方便。如果其中一个意图无法执行,整个操作将取消并返回错误。
let coordinator = NSFileCoordinator()
let readIntent = NSFileCoordinator
.ReadingIntent(url: sourceURL)
let writeIntent = NSFileCoordinator
.WritingIntent(url: destURL)
coordinator.coordinate(
with: [readIntent, writeIntent]
) { error in
try? FileManager.default
.copyItem(at: sourceURL, to: destURL)
}
异步协调 — 从 iOS 15 开始,NSFileCoordinator 支持带有 completion handler 的异步方法,允许在不阻塞调用线程的情况下执行协调。这对 UI 线程至关重要,因为同步等待协调可能导致界面冻结数秒。
NSFilePresenter — 是一个对象实现的协议,用于接收由 NSFileCoordinator 协调的文件更改通知。如果您的应用程序显示可能被其他进程修改的文件内容(例如 iCloud Drive 同步新版本),实现 NSFilePresenter 可以及时更新界面。
class DocumentPresenter: NSFilePresenter {
let presentedItemURL: URL?
let presentedItemOperationQueue: OperationQueue
init(url: URL) {
presentedItemURL = url
presentedItemOperationQueue = OperationQueue()
}
func presentedItemDidChange() {
DispatchQueue.main.async {
NotificationCenter.default
.post(name: .documentDidChange,
object: self)
}
}
func presentedItemDidMove(to newURL: URL) {
Logger.storage.info(
"File moved to: \(newURL.lastPathComponent)"
)
}
func accommodatePresentedItemDeletion(
completionHandler: @escaping (Error?) -> Void
) {
Logger.storage.warn("File deleted externally")
completionHandler(nil)
}
}
协议方法: presentedItemDidChange 在文件内容更改时调用,presentedItemDidMove(to:) — 在文件移动后调用,accommodatePresentedItemDeletion — 在另一个进程删除文件之前调用(允许应用程序正确关闭文件)。此外,协议通过 presentedItemDidGainVersion: 和 presentedItemDidLoseVersion: 支持版本管理。
重要: NSFilePresenter 必须通过 NSFileCoordinator.addFilePresenter: 在系统中注册。未注册时将不会传递通知。注册在应用程序启动时进行一次,在重新创建呈现器时无需重新注册。
始终使用协调器 处理 Ubiquity container (iCloud Drive) 中和扩展可访问目录中的文件。即使应用程序当前是单线程的,未来的更新或系统更改可能会添加并行访问,缺乏协调将导致难以发现的错误。
最小化在协调块中的时间。 在执行块期间,其他进程无法访问该文件。块内的长时间操作(复杂数据处理、网络请求)会阻塞整个文件访问系统。只在块内执行数据读取或写入,处理操作在块外进行。
避免死锁: 不要从另一个协调器的块内部为同一文件调用协调器——这将导致相互阻塞。使用批量操作(意图数组)而不是嵌套调用。如果嵌套是必要的,请使用不同的队列或不同的 URL。
根据 objc.io (2024),使用 NSFileCoordinator 时的典型错误包括:completion handler 中缺少错误处理(导致操作未完成);仅为写入而不是读取进行协调;在 UI 线程中使用过时的同步 API;使用 iCloud Drive 时忽略 NSFilePresenter 协议。最后一个错误最隐蔽:应用程序显示过时的数据,却不知道文件已被修改。
常见问题
NSFileCoordinator — 用于从多个线程或进程安全访问文件的 Foundation 类。通过在文件系统级别协调读写操作来防止竞态条件。
NSLock 仅在一个进程内(线程之间)工作。NSFileCoordinator 协调不同进程和扩展之间的访问,包括 iCloud Drive 同步和 File Provider Extension。
是的,Apple 强烈建议对 Ubiquity container 的所有文件操作使用 NSFileCoordinator。没有协调器,设备间同步时可能出现数据损坏以及与 File Provider Extension 的冲突。
NSFilePresenter — 用于接收文件更改通知的协议。允许应用程序响应其他进程所做的更改:在修改时更新 UI、处理移动或准备删除文件。
五种类型: ReadingIntent(读取)、WritingIntent(写入)、带 .withoutChanges 的 ReadingIntent(不跟踪读取)、带 .contentIndependentMetadataOnly 的 WritingIntent(仅元数据)和带 .forDeleting 的 WritingIntent(删除)。每个都定义了文件的访问级别。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。