大多数浏览器和
Developer App 均支持流媒体播放。
-
使用 CKSyncEngine 同步到 iCloud
了解 CKSyncEngine 如何帮助你将用户的 CloudKit 数据同步到 iCloud。了解在系统处理同步操作调度时如何减少 App 中的代码量。我们将与你分享如何随着 CloudKit 的发展自动从增强的性能中受益,探索用于同步实施的测试等。为了让本次讲座发挥最大价值,你需要提前了解 CloudKit 和 CKRecord 类型。
章节
- 0:00 - Intro
- 0:48 - The state of sync
- 3:09 - Meet CKSyncEngine
- 9:47 - Getting started
- 13:18 - Using CKSyncEngine
- 19:25 - Testing and debugging
- 22:27 - Wrap-up
资源
相关视频
Tech Talks
-
下载
♪ ♪
Tim:大家好 我是 Tim CloudKit 团队的一名工程师 今天 我和我的同事 Aamer 会为你介绍一个新的 CloudKit API CKSyncEngine CKSyncEngine 旨在帮助 同步设备和云之间的数据 首先 我会谈谈在 Apple 平台上 与 CloudKit 同步的状态 接着 我会介绍 CKSyncEngine 的 定义和工作原理 然后 你会了解如何在自己的项目中 开始使用 CKSyncEngine 并且 在完成设置后 你会学习如何使用同步引擎 在设备之间同步数据 最后 你会了解用 CKSyncEngine 进行测试和调试集成的最佳实践 首先 我们来介绍一下 与 CloudKit 同步的状态
在你构建新的 App 时 用户希望他们的数据能够同步 他们会在 iPhone 上 创建一些内容 并希望在打开 Mac 时 也能够看到同样的内容 作为外行来看 这个过程就像变魔术一样 他们的数据先是在一个地方 然后传送到各个地方 但对于你和我而言 这个过程远远没有那么简单 CloudKit 本身并不复杂 但是实现同步通常却很困难 当你将多个设备带入同一场景 就会出现很多问题 所以 你的同步代码 应该越简单越好 而简化同步代码的最佳方法 就是尽可能地控制代码量 不过好在 一些出色的 API 可以帮助你与 CloudKit 进行同步 并为你承担许多繁重的工作 如果你想要一个包含本地持久化的 全栈式解决方案 你可以使用 NSPersistentCloudKitContainer 如果你想要引入自己的本地持久化 你可以使用 新的 CKSyncEngine API 如果你还需要更细粒度的控制 你可以使用 CKDatabase 和 CKOperations 但如果你想与 CloudKit 同步 而没有使用 NSPersistentCloudKitContainer 那你应该使用 CKSyncEngine 同步涉及许多不同的内容 使用 CKSyncEngine 等 更高级的 API 可以帮助你减少复杂度 并改善 App 的同步体验
就其本质而言 同步基本就是从一个设备发送更改 然后从另一个设备获取更改 并在必要时 与 CloudKit 记录相互转换 单纯实现这一点并不难 但是实际要实现的内容远不止于此 你需要了解所有的操作和错误 监控系统条件 关注账户变化 处理推送通知 管理订阅 并跟踪大量状态等 在你使用 CKSyncEngine 时 你所编写的同步代码量 会变得更少且更集中 并且 你只需处理 与 App 特定相关的事情 其余部分交给同步引擎即可 为了编写一个合适的同步引擎 你可能需要几千行的代码 并在测试过程中使用双倍的代码 实际上 我曾经听说 NSPersistentCloudKitContainer 由 70,000 多行测试代码支持 CKSyncEngine 的测试代码 应该也很长 这是因为其需要为你 处理大量这样的工作 所以 这个新的 CKSyncEngine API 到底是什么呢?
CKSyncEngine 封装了 与 CloudKit 数据库同步的通用逻辑 它旨在提供方便的 API 并在必要时赋予其灵活性 并且 该 API 的设计是为了 满足大部分 App 的需求 否则 这些 App 就需要 自己编写自定义同步引擎 通常来说 如果你想要 同步 App 的私有和共享数据 CKSyncEngine 会是一个不错的选择 你可用于与同步引擎 结合使用的数据模型 由记录和区域组成 其所使用的数据类型 与 CloudKit 的其他部分相同 你可以使用任何现有的 CloudKit API 来访问这些数据 因此 如果你已经有了一个 现有的 CloudKit 同步实现 那么 CKSyncEngine 也可以与之实现同步 系统中的一些 App 和服务 现已开始使用同步引擎 其中包括 无边记 App 另一个例子是 NSUbiquitousKeyValueStore 它是基于同步引擎重新编写的 并且 这也是一个 很好的向后兼容用例示例 虽然该类在较新版的 OS 中 使用的是同步引擎 但其仍然能够 与此前的版本实现同步 所以 如果你已经有了 自定义 CloudKit 同步实现 你就可以选择 切换到 CKSyncEngine 如果你觉得以上优势足够吸引人 那么你就可以考虑进行切换 但这也不是必需的 有时 只需要 维护较少的代码就不错 你还可以 从 CKSyncEngine 的新增强中获益 同步引擎随平台的发展而不断改进 在此过程中 同步也变得更加简单高效 此外 你还可以受益于 CKSyncEngine 中较小的 API 接口 从而可以专注于 App 中 特定的数据模型和用例 如果你正在考虑 使用 CKSyncEngine 但部分特定需求却无法得到支持 如果你愿意的话 你也可以自己进行编译 但如果你认为 CKSyncEngine 的 新功能可以满足你的需求 你就可以考虑将该用例填入反馈中 毕竟 同步引擎中的一些最佳想法 都是来自像你这样的开发者 那么 同步引擎的 实际工作原理是什么呢? 通常情况下 同步引擎会充当 App 和 CloudKit 服务器 之间的数据传输通道 你的 App 会在记录和区域方面 与同步引擎进行通信 如果有更改需要保存 你的 App 便会将其提供给同步引擎 如果同步引擎 从其他设备上获取到这些更改 其就会再将这些更改 提供给你的 App 也就是说 同步引擎 在需要执行其他任务时 无法立即执行上述操作 如果同步引擎需要 与服务器进行通信 那么其首先就会和 系统任务调度器进行协商 由于 OS 也使用该调度器 进行后台任务管理 从而 其便可以确保 设备已经准备好进行同步 在设备准备就绪之后 调度器就会运行任务 并且同步引擎也会 与服务器进行通信 以上就是同步引擎的基本操作流程 如果更具体一点 同步引擎向服务器 发送更改时的流程是什么呢? 首先 用户对数据进行了修改 他们或输入内容 或切换开关 或删除对象 接着 你的 App 就会告知同步引擎 有一个挂起的更改 需要发送给服务器 这样 同步引擎便知道 需要开始工作了 接下来 同步引擎就会 向调度器提交任务 在设备就绪之后 调度器就会运行该任务 在任务运行过程中 同步引擎就会启动 向服务器发送更改的进程 为此 同步引擎会向你的 App 请求下一批要发送的更改 如果用户只进行了一项修改 那么你可能只有一个挂起的更改 但如果用户导入了 包含新数据的大型数据库 那么你就可能会有成百上千个更改 由于单个请求 对发送给服务器的数据量有限制 因此 同步引擎会按批次 请求这些更改 这也有助于减少内存成本 因为这些记录在实际需要之前 都不会加载到内存中 在你提供完下一批之后 同步引擎就会将其发送给服务器 接着 服务器就会 响应改操作的结果 其中包括这些更改 成功或失败的全部信息 在完成请求之后 同步引擎就会 回调你的 App 并提供结果 这时 你便可以对操作的 成功或失败做出反应 如果你还有挂起的更改 同步引擎就会继续请求批次 直到没有需要发送的更改为止 现在 一个设备已经 将数据发送给了服务器 接着 其他设备就可以获取该数据 在服务器收到新的更改后 其就会向具有数据访问权限的 其他设备发送推送通知 由于 CKSyncEngine 能够 自动监听 App 中的这些推送通知 所以 其在收到通知后 就会将任务提交给调度器 在调度器运行这些任务时 同步引擎就可以从服务器中获取数据 在其获取到新的更改后 便会将更改传递给你的 App 这时 你便可以本地持久化这些更改 并将其显示在 UI 中 以上就是使用同步引擎时的 基本操作流程 所有这些流程都有一个共同点 即系统调度器 一般情况下 CKSyncEngine 会在进行操作之前与调度器协商 从而其便可以 以你的名义自动进行同步
调度器会对网络连接、电池电量 以及资源使用等系统状况进行监视 从而可以确保在进行同步前 设备已经满足了所有先决条件 通过遵循调度器的安排 同步引擎便可以保证 在用户体验和设备资源之间 实现适当的平衡 正常情况下 同步过程非常快速 通常在几秒之间便可完成 但是 如果没有网络连接 或是设备电量不足 同步可能就会延迟或推迟 如果设备负载过重 而你不希望同步机制干扰到 App 中的其他紧急任务 借助同步引擎的自动安排 你便可以放心地进行同步操作 即需要同步的时候进行同步 不需要的时候就不同步 这样 同步引擎不仅效率更高 并且使用也更加简便 如果无需担心何时进行同步 你便可以集中精力处理其他任务 这也就是说 手动执行同步 是有合理用例的 这样 你可能有一个 可立刻获取数据的下拉刷新 UI 或你现在有一个 可立即向服务器 发送挂起更改的备份按钮 在编写自动化测试时 手动同步也可以发挥作用 在需要控制事件顺序的情况下 其可以帮助你模拟 跨多个设备的特定同步场景 一般情况下 我们建议你 依赖自动同步安排 但我们知道 手动同步也有合理的用例 所以 同步引擎也为这种必要的情况 提供了 API 接下来 Aamer 将向你介绍 如何开始使用 CKSyncEngine Aamer:Tim 谢谢你的介绍 我是 Aamer CloudKit Client 团队的 一名工程师 接下来 我会向你介绍 如何开始使用 CKSyncEngine 在使用 CKSyncEngine 之前 你需要进行一些项目设置 无论你是使用 CKSyncEngine 或是编译自定义的 CloudKit 实现 这些要求都是一样的 首先 你需要对 CloudKit 基本数据类型、 CKRecord 和 CKRecordZone 有基本的了解 由于同步引擎 API 主要处理 记录和区域方面的事项 所以你在深入探索之前 需要了解其定义是什么 接着 你需要在 Xcode 中 为项目启用 CloudKit 功能 最后 由于同步引擎 通过推送通知来保持更新 所以 你还需要启用远程通知功能 在完成所有这些步骤后 你就可以初始化同步引擎了 你需要在启用 App 之后 尽快初始化 CKSyncEngine 在你初始化同步引擎时 该引擎就会自动开始监听推送通知 以及后台的调度器任务 由于这些通知和任务 随时都有可能发生 所以 你需要初始化同步引擎 来对其进行处理
CKSyncEngineDelegate 协议 是你的 App 与 CKSyncEngine 进行通信的主要方式 在初始化同步引擎时 你需要提供一个遵循该协议的对象 为了能够正常高效地运行 同步引擎会对部分内部状态 进行跟踪 同时 你还需要提供同步引擎状态的 最新已知版本
在执行同步操作过程中 同步引擎偶尔会 以状态更新事件的形式 为代理对象提供该状态的更新版本 每当同步引擎 给出新的状态序列化时 你都应该对其进行本地持久化 这样 你就可以在下次启动进程时 提供该状态序列化 并对同步引擎进行初始化 为了让你更好地理解这个过程 我会通过代码示例来进行解释
为了初始化同步引擎 你需要传入一个配置对象 在配置中 你需要提供想要实现同步的数据库、 同步引擎状态的最新已知版本 以及代理对象 代理协议中的一个函数 是 handleEvent 函数 该函数指明了 同步引擎如何通知 App 正常同步操作期间发生的各种事件 例如 当引擎从服务器获取新的数据 或账户发生更改时 同步引擎就会发布事件 状态更新事件是其中的一种事件 在同步引擎更新其内部状态 或你更新自身状态时 同步引擎就会发布状态更新事件 为了响应该事件 你便需要 将该新版本的状态序列化 进行本地持久化 在本示例中 你会在下次初始化同步引擎时 使用该状态序列化 现在基础设置已经完成了 接下来 我会为你介绍如何与同步引擎同步
你可以通过以下几个简单步骤 来将更改发送给服务器 首先 在同步引擎状态中 添加挂起的记录区域更改 和挂起的数据库更改 以便提醒同步引擎安排同步操作 同步引擎会确保一致性 并对这些更改进行去重
接着 实现委托方法 nextRecordZoneChangeBatch 同步引擎会调用该方法 来获取下一批发送给服务器的 记录区域更改 最后 处理 sentDatabaseChanges 和 sentRecordZoneChanges 事件 这些事件会在更改发送到服务器之后 立刻进行发布
这里的示例介绍了 如何向服务器发送更改 此 App 可以编辑数据 并需要同步新的记录更改 为了实现这一点 你需要 向同步引擎状态添加挂起的 记录区域更改 来告知同步引擎需要保存该记录 在同步引擎准备好同步记录后 其会调用委托方法 nextRecordZoneChangeBatch 这里 你还需要返回 下一批发送给服务器的更改 你需要提供挂起的更改列表 和记录提供程序 以初始化 RecordZoneChangeBatch 挂起的更改列表包含 需要保存或删除的 recordID 以及同步发生时 用于将 ID 映射到记录中的 记录提供程序 以下介绍的是 App 从服务器 获取更改的方式 同步引擎可以自动为你 从服务器获取更改 在此过程中 其会发布 fetchedDatabaseChanges 和 fetchedRecordZoneChanges 事件 根据你的用例 你可能希望监听 willFetchChanges 和 didFetchChanges 事件 例如 如果你希望 在获取更改之前或之后 执行设置或清除任务 那么处理上述事件 便会对你有所帮助 这里的示例介绍了 App 从服务器获取更改的位置 在同步引擎从记录区域获取更改时 其便会发布 fetchedRecordZoneChanges 事件 该事件包含由其他设备执行的 修改和删除 在监听该事件时 你需要检查获取的修改 和删除 当你收到修改时 你便需要对数据进行本地持久化 收到删除时 你便需要本地删除该数据 获取数据库更改的流程与之类似 并可以使用相同的方法进行处理 但处理错误可能会很麻烦 不过同步引擎也可以为你提供帮助 同步引擎可以自动处理 网络问题、限流 以及账户问题等瞬时错误 同步引擎会自动重试 受到以上错误影响的任务 你的 App 需要 处理其他类型的错误 在解决完这些错误后 你需要在必要时 对任务重新进行安排
这里的示例介绍了 发送记录区域更改的错误处理 当发布 sendRecordZoneChanges 事件时 你需要检查 failedRecordSaves 以查看是否存在保存失败的记录
ServerRecordChanged 表示服务器上的记录发生了更改 这就意味着 另一台设备保存了 App 尚未获取的新版本 你需要解决该冲突 并重新安排任务
ZoneNotFound 表示服务器上尚不存在该区域 为了解决该问题 你需要创建区域 并重新安排任务 同步引擎会先保存区域 然后保存记录 networkFailure、 networkUnavailable、 serviceUnavailable 和 requestRateLimited 都是同步引擎可为你解决的 瞬时错误示例 你仍会收到这些错误以供了解情况 但你无需采取任何措施 来对其进行响应 同步引擎会在 系统条件允许的情况下 自动重试这些错误
此外 同步引擎还可以 帮助你处理账户变更 iCloud 账户可能随时 在设备上发生变更 同步引擎可以帮助你 管理并响应这些变更 同步引擎会监听变更 并通过 accountChange 事件 来通知你进行登入、登出 或账户切换 你的 App 需要根据变更类型 进行相应准备
除非设备上存在账户 否则同步引擎 不会与 iCloud 进行同步
你可以随时对同步引擎进行初始化 并且其会在账户变更时自动通知你 与其他用户共享数据 是 CloudKit 的重要部分 同步引擎在这一方面 也能够让你的生活更简单 由于同步引擎可与 CloudKit 共享数据库结合使用 所以 你只需要为 App 所使用的 每个数据库创建一个同步引擎即可 例如 你可以为私有数据库 创建一个同步引擎 并为共享数据库 创建另外一个同步引擎 想要进一步了解有关 使用 CloudKit 实现共享的信息 请查看“充分利用 CloudKit 共享”科技讲座
其中介绍了 CKSyncEngine 的使用方法 接下来 我将为你介绍 如何在使用该类时进行测试 自动化测试是在快速开发过程中 用于确保代码库稳定的最佳方式 通过同步引擎 你可以使用 多个 CKSyncEngine 实例 来模拟设备之间的用户流程
并且 你需要模拟 App 可能遇到的边缘情况 为了实现这一点 你可以 将 automaticallySync 设置为 false 来干预同步引擎的流程 这个测试示例 模拟了两个设备及服务器之间的 数据冲突 进行该测试是为了模拟 用户在使用多个设备时 所采取的完整流程 并且 该测试也会对 冲突解决方案进行验证 首先 使用 MySyncManager 模拟两个设备 在本示例中 MySyncManager 会创建一个本地数据库和同步引擎 deviceA 将 value 设置为 A 并将其更改发送到服务器
在 deviceB 从服务器 获取更改前 我们也会要求其 将更改发送到服务器 由于 deviceA 先保存到服务器 所以 deviceB 的保存 应该会失败 从而便会出现服务器记录更改错误 而这会触发本地冲突解决代码 由于本示例希望冲突解决方案 优先处理服务器中的数据 所以 deviceB 中的新值 就是 deviceA 最新 向服务器发送的值
以下几个要点可以帮助你 加快测试和调试的速度 了解事件在每个设备上 发生的顺序可以帮助你 在流程中准确定位 App 可能发生问题的位置 在开发过程中尽量记录日志 可以帮你跟踪这些流程 并比较多个设备间的日志 尽管 CloudKit 会记录 你收到的所有事件 但你还应该记录 App 中 与此类事件相关的操作
记录记录 ID 和区域 ID 可以帮助你 调试同步引擎、服务器 及其他可能用于同步的设备 之间的传输数据类型
编写模拟每种用户流程的测试 可以让你在扩大代码库时 保持稳定性
解决问题时应注意观察时间戳 你可能只进行了几次同步操作 也可能在很短的时间内进行了多次 确保跟踪正确的操作 是在多个设备中进行调试的关键
这些步骤可以帮助你 借助 CKSyncEngine 创建并维护可靠且持久的 App
以上就是 关于 CKSyncEngine 的全部内容 请查看同步引擎示例代码 以了解 App 中的完整工作示例 如果你想要进一步了解 请查阅 CKSyncEngine 文档 如果你对改进同步引擎有任何建议 请向 CloudKit 团队提交反馈 我们很高兴看到 你使用其创造出的作品 感谢你的观看 祝你在 WWDC 中度过愉快的时光! ♪ ♪
-
-
12:14 - Initializing CKSyncEngine
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 } } }
-
14:13 - Sending changes to the server
func userDidEditData(recordID: CKRecord.ID) { // Tell the sync engine we need to send this data to the server. 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) } }
-
15:40 - Fetching changes from the server
func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async { switch event { case .fetchedRecordZoneChanges(let recordZoneChanges): for modifications in recordZoneChanges.modifications { // Persist the fetched modification locally } for deletions in recordZoneChanges.deletions { // Remove the deleted data locally } case .fetchedDatabaseChanges(let databaseChanges): for modifications in databaseChanges.modifications { // Persist the fetched modification locally } for deletions in databaseChanges.deletions { // Remove the deleted data locally } // Perform any setup/cleanup necessary 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 { // Merge server record into local data syncEngine.state.add(pendingRecordZoneChanges: [ .save(recordID) ]) } case .zoneNotFound: // Tried to save a record, but the zone doesn't exist yet. syncEngine.state.add(pendingDatabaseChanges: [ .save(recordID.zoneID) ]) syncEngine.state.add(pendingRecordZoneChanges: [ .save(recordID) ]) // CKSyncEngine will automatically handle these errors case .networkFailure, .networkUnavailable, .serviceUnavailable, .requestRateLimited: break // An unknown error occurred default: break } } case .accountChange(let event): switch event.changeType { // Prepare for new user case .signIn: break // Delete local data case .signOut: break // Delete local data and prepare for new user case .switchAccounts: break } } }
-
18:49 - Using CKSyncEngine with private and shared databases
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) }
-
20:00 - Testing CKSyncEngine integration
func testSyncConflict() async throws { // Create two local databases to simulate two devices. let deviceA = MySyncManager() let deviceB = MySyncManager() // Save a value from the first device to the server. deviceA.value = "A" try await deviceA.syncEngine.sendChanges() // Try to save the value from the second device before it fetches changes. // The record save should fail with a conflict that includes the current server record. // In this example, we expect the value from the server to win. deviceB.value = "B" XCTAssertThrows(try await deviceB.syncEngine.sendChanges()) XCTAssertEqual(deviceB.value, "A") }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。