Highlight
CKSyncEngine 是 Apple 为 CloudKit 推出的高层同步引擎,开发者只需处理应用特定的数据转换逻辑,系统会自动处理推送监听、任务调度、网络重试、批量传输和冲突解决,将原本需要数千行代码的自定义同步实现缩减到几百行。
核心内容
用户期待数据能在所有设备上同步。在 iPhone 上记了一条笔记,打开 Mac 时它应该已经在那里。这种体验看起来理所当然,但实现起来很复杂。
CloudKit 本身不算复杂,但同步涉及很多细节:网络状态监控、电池管理、推送通知处理、订阅管理、状态追踪、错误恢复、账号切换等等。一个完整的同步引擎可能需要几千行代码,测试代码还要翻倍。Apple 内部透露 NSPersistentCloudKitContainer 的测试代码超过 7 万行。
WWDC23 推出的 CKSyncEngine 把这些通用逻辑封装起来,让你只关心应用特有的部分。
Apple 平台上有三个层级的 CloudKit 同步方案:
- NSPersistentCloudKitContainer:全栈方案,包含本地持久化,适合直接用 Core Data 的应用
- CKSyncEngine:自带本地持久化的同步引擎,适合需要自定义存储方案的应用
- CKDatabase + CKOperations:完全手动控制,只有 CKSyncEngine 无法满足需求时才用
如果你已经在用 NSPersistentCloudKitContainer,继续用就好。如果你有自己的本地存储方案(SQLite、文件系统等),CKSyncEngine 是最佳选择。
CKSyncEngine 已经被多个系统应用采用,包括 Freeform 和 NSUbiquitousKeyValueStore。它使用标准的 CKRecord 和 CKRecordZone,可以和已有的 CloudKit 实现互通。随着平台演进,CKSyncEngine 会自动获得性能优化和新功能。
同步引擎的工作流程
(05:11)CKSyncEngine 的核心是数据管道。你的应用用 record 和 zone 与引擎通信,引擎负责与服务器交互。
发送变更的流程:
- 用户修改数据(输入文字、切换开关、删除对象)
- 应用告诉 sync engine 有待发送的变更
- Sync engine 向系统调度器提交任务
- 设备就绪后,调度器运行任务
- Sync engine 向应用批量请求变更数据
- 数据发送到服务器
- 服务器返回结果,sync engine 回调给应用
(06:45)批量处理是关键设计。如果用户导入了大量数据,可能有成百上千条变更。Sync engine 分批请求,避免一次性加载所有记录到内存。应用只在引擎请求时才提供下一批数据。
接收变更的流程:
- 服务器收到新变更后发送推送通知
- CKSyncEngine 自动监听推送
- 收到通知后提交任务给调度器
- 调度器运行后,sync engine 从服务器获取变更
- 变更交给应用持久化到本地
(08:00)系统调度器是这一切的基础。它监控网络连接、电池电量、资源占用等条件,确保在合适的时机同步。正常情况下同步在几秒内完成,但如果网络断开或电量低,同步会被推迟。你不需要自己处理这些逻辑。
当然,也有需要立即同步的场景:下拉刷新、立即备份按钮。CKSyncEngine 提供了手动同步的 API,但 Apple 推荐优先依赖自动调度。
初始化与配置
(12:14)使用 CKSyncEngine 需要几个前提:了解 CKRecord 和 CKRecordZone、在 Xcode 中启用 CloudKit 和远程推送能力。
初始化代码:
actor MySyncManager: CKSyncEngineDelegate {
init(container: CKContainer, localPersistence: MyLocalPersistence) {
let configuration = CKSyncEngine.Configuration(
database: container.privateCloudDatabase,
stateSerialization: localPersistence.lastKnownSyncEngineState,
delegate: self
)
self.syncEngine = CKSyncEngine(configuration)
}
func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
switch event {
case .stateUpdate(let stateUpdate):
self.localPersistence.lastKnownSyncEngineState = stateUpdate.stateSerialization
}
}
}
关键点:
- 初始化时要提供数据库、上次保存的状态序列化、delegate
- State serialization 必须持久化到本地,下次启动时恢复
- Delegate 的
handleEvent是主要的通信接口 - 建议在应用启动后尽快初始化 sync engine,这样它才能监听推送和调度任务
发送变更到服务器
(14:13)发送变更分三步:标记待发送变更、实现批量提供方法、处理发送结果。
func userDidEditData(recordID: CKRecord.ID) {
// 告诉 sync engine 需要发送这条数据
self.syncEngine.state.add(pendingRecordZoneChanges: [ .save(recordID) ])
}
func nextRecordZoneChangeBatch(
_ context: CKSyncEngine.SendChangesContext,
syncEngine: CKSyncEngine
) async -> CKSyncEngine.RecordZoneChangeBatch? {
let changes = syncEngine.state.pendingRecordZoneChanges.filter {
context.options.zoneIDs.contains($0.recordID.zoneID)
}
return await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: changes) { recordID in
self.recordToSave(for: recordID)
}
}
关键点:
pendingRecordZoneChanges告诉引擎哪些 record 需要同步- Sync engine 会自动去重和保持一致性
nextRecordZoneChangeBatch在引擎准备好发送时被调用RecordZoneChangeBatch用 pending changes 和 record provider 初始化,provider 在真正需要时才加载 record
获取服务器变更
(15:40)获取变更是自动的。Sync engine 会发送 fetchedRecordZoneChanges 和 fetchedDatabaseChanges 事件:
func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
switch event {
case .fetchedRecordZoneChanges(let recordZoneChanges):
for modifications in recordZoneChanges.modifications {
// 将修改持久化到本地
}
for deletions in recordZoneChanges.deletions {
// 从本地删除数据
}
case .fetchedDatabaseChanges(let databaseChanges):
for modifications in databaseChanges.modifications {
// 处理 database 级别的修改
}
for deletions in databaseChanges.deletions {
// 处理 database 级别的删除
}
case .willFetchChanges, .didFetchChanges:
// 在获取前后执行设置/清理工作
break
case .sentRecordZoneChanges(let sentChanges):
for failedSave in sentChanges.failedRecordSaves {
let recordID = failedSave.record.recordID
switch failedSave.error.code {
case .serverRecordChanged:
if let serverRecord = failedSave.error.serverRecord {
// 合并服务器记录到本地数据
syncEngine.state.add(pendingRecordZoneChanges: [ .save(recordID) ])
}
case .zoneNotFound:
// Zone 不存在,先创建 zone 再重试
syncEngine.state.add(pendingDatabaseChanges: [ .save(recordID.zoneID) ])
syncEngine.state.add(pendingRecordZoneChanges: [ .save(recordID) ])
// 以下错误 sync engine 会自动重试
case .networkFailure, .networkUnavailable, .serviceUnavailable, .requestRateLimited:
break
default:
break
}
}
case .accountChange(let event):
switch event.changeType {
case .signIn:
// 准备新用户数据
break
case .signOut:
// 清除本地数据
break
case .switchAccounts:
// 清除数据并准备新用户
break
}
}
}
关键点:
fetchedRecordZoneChanges包含 modifications 和 deletions 两个数组serverRecordChanged是最常见的冲突,需要应用自己决定合并策略zoneNotFound需要先创建 zone 再重试保存 record- 网络类错误(networkFailure、serviceUnavailable 等)sync engine 会自动处理
accountChange事件处理账号登录、登出、切换三种情况
支持 Private 和 Shared 数据库
(18:49)可以为每个数据库创建独立的 sync engine:
let databases = [ container.privateCloudDatabase, container.sharedCloudDatabase ]
let syncEngines = databases.map {
var configuration = CKSyncEngine.Configuration(
database: $0,
stateSerialization: lastKnownSyncEngineState($0.databaseScope),
delegate: self
)
return CKSyncEngine(configuration)
}
关键点:
- 每个数据库需要独立的 sync engine 实例
- 每个实例需要独立的状态序列化存储
- Shared database 用于 CloudKit Sharing 场景
测试
(20:00)可以用多个 sync engine 实例模拟多设备场景:
func testSyncConflict() async throws {
// 创建两个本地数据库模拟两台设备
let deviceA = MySyncManager()
let deviceB = MySyncManager()
// 设备 A 先保存到服务器
deviceA.value = "A"
try await deviceA.syncEngine.sendChanges()
// 设备 B 在获取变更前也尝试保存
deviceB.value = "B"
XCTAssertThrows(try await deviceB.syncEngine.sendChanges())
XCTAssertEqual(deviceB.value, "A")
}
关键点:
- 创建多个 sync manager 模拟多设备
- 设置
automaticallySync = false可以精确控制同步时机 - 测试冲突解决逻辑时,验证本地数据是否按预期更新
- 记录 record ID 和 zone ID 有助于调试多设备同步问题
详细内容
错误处理策略
CKSyncEngine 自动处理瞬态错误:
- networkFailure / networkUnavailable:网络问题,自动重试
- serviceUnavailable:服务不可用,自动重试
- requestRateLimited:请求频率限制,自动重试
- account issues:账号问题,自动处理
需要应用处理的错误:
- serverRecordChanged:服务器记录已被修改,需要应用决定合并策略
- zoneNotFound:zone 不存在,需要先创建
- unknown errors:未知错误,需要应用决定如何处理
状态管理
Sync engine 的内部状态通过 stateSerialization 保存。这个状态包含:
- 已知的 server change token
- 待发送的变更队列
- 订阅信息
- 其他内部追踪数据
每次状态更新时,sync engine 会发送 stateUpdate 事件。你必须持久化这个状态,否则下次启动时 sync engine 会重新全量同步。
调试技巧
- 记录事件序列:在
handleEvent中记录每个事件的类型和时间戳 - 记录 record ID 和 zone ID:帮助追踪数据流向
- 对比多设备日志:用时间戳对齐不同设备的操作
- 使用 Push Notifications Console:验证推送是否正常到达
核心启发
1. 给现有本地存储应用添加 iCloud 同步
- 做什么:把原来只存本地的笔记、待办、习惯追踪等应用加上跨设备同步
- 为什么值得做:CKSyncEngine 把同步复杂度降到最低,你只需处理数据转换,就能获得和系统应用同级别的同步体验
- 怎么开始:定义 CKRecord 转换逻辑,初始化 CKSyncEngine,在数据变更时标记 pending changes
2. 做一个支持协作的文档编辑器
- 做什么:类似 Freeform 的多人协作白板或文档应用
- 为什么值得做:CKSyncEngine 支持 shared database,结合 CloudKit Sharing 可以实现多用户实时协作
- 怎么开始:用 private database 存储个人数据,用 shared database 存储协作数据,为每个 database 创建独立的 sync engine
3. 做一个离线优先的相册/收藏应用
- 做什么:图片收藏、链接收藏、音乐收藏等应用,支持离线浏览和自动同步
- 为什么值得做:Sync engine 的自动调度确保在网络恢复时无缝同步,批量处理适合大量媒体元数据
- 怎么开始:本地用 SwiftData 或 Core Data 存储,CKSyncEngine 同步 record 到 CloudKit,媒体文件用 CKAsset
4. 重写自定义同步方案
- 做什么:把原来自己维护的 CloudKit 同步代码迁移到 CKSyncEngine
- 为什么值得做:减少维护成本,自动获得系统级优化,测试覆盖更完善
- 怎么开始:逐步替换:先让 CKSyncEngine 和现有实现共存,验证数据一致性后再完全切换
关联 Session
- Get the most out of CloudKit Sharing — CloudKit 数据共享的完整实现方案
- What’s new in Core Data — Core Data 和 NSPersistentCloudKitContainer 的新特性
- Meet Push Notifications Console — 调试推送通知的图形化工具
- Build robust and resumable file transfers — 大文件传输的最佳实践
评论
GitHub Issues · utterances