大多数浏览器和
Developer App 均支持流媒体播放。
-
将 App 迁移到 Swift 6
以更新现有示例 App 为例,体验迁移到 Swift 6 的实际过程。了解如何循序渐进地逐个模块进行迁移,以及编译器如何帮你识别存在数据争用风险的代码。探索有哪些不同的技巧可用于确保隔离边界清晰,并避免对共享的可变状态进行并发访问。
章节
- 0:00 - Introduction
- 0:33 - The Coffee Tracker app
- 0:45 - Review the refactor from WWDC21
- 3:20 - Swift 6 and data-race safety
- 4:40 - Swift 6 migration in practice
- 7:26 - The strategy
- 8:53 - Adopting concurrency features
- 11:05 - Enabling complete checking in the watch extension
- 13:05 - Shared mutable state in global variables
- 17:04 - Shared mutable state in global instances and functions
- 19:29 - Delegate callbacks and concurrency
- 23:40 - Guaranteeing data-race safety with code you don’t maintain
- 25:51 - Enabling the Swift 6 language mode in the watch extension
- 26:35 - Moving on to CoffeeKit
- 27:24 - Enabling complete checking in CoffeeKit
- 27:47 - Common patterns and an incremental strategy
- 29:55 - Global variables in CoffeeKit
- 31:05 - Sending an array between actors
- 33:53 - What if you can’t mark something as Sendable?
- 35:23 - Enabling the Swift 6 language mode in CoffeeKit
- 35:59 - Adding a new feature with guaranteed data-race safety
- 40:43 - Wrap up and the Swift 6 migration guide
资源
相关视频
WWDC21
-
下载
嗨 我是 Ben 来自 Swift 团队 在本视频中 我将引导大家 在现有应用程序中 启用 Swift 6 语言模式 我们将了解 Swift 6 如何帮助你 规避可能出现的争用情况 同时我们将探讨一些技巧 以便将这个变化 逐步引入你的 App 此外 由于一些框架尚未考虑到 Swift 的并发保证 我们也将了解如何处理 与这些框架的交互 我会以一个简单的 App 为例 来介绍相关内容 这个 App 可用于跟踪 全天的咖啡饮用情况 同时提供一个复杂功能 用来 在表盘上显示当前的咖啡因水平 我们在 WWDC 2021 上 首次推出了 Swift 并发 当时 我曾向大家介绍过如何在这个 App 中采用 Swift 新的并发模型 在那场讲座中 我们发现 一个看似简洁的 App 架构 有时会掩盖 并发的潜在复杂性 如果只看视图和模型 一切都井井有条 但深入了解并发的管理方式后 你就会有不一样的发现 原始 App 有 3 个并发队列 而代码也会在其中执行 与 UI 和模型相关的工作 已在主队列中完成 这个 App 还有一个调度队列 它可用于在后台处理任务 最后 针对完成处理程序 的某些回调 比如用于从 HealthKit 返回结果的回调 则会在任意队列中完成 因此 虽然类型架构整洁有序 但在整个 App 中 并发任务的安排方式 却并非同样清晰明了
哪个队列在执行代码 又为何会执行代码 这些问题会在不同位置 以不甚清晰的方式交织在一起
通过采用 Swift 并发 我们可摆脱这种 Ad Hoc 并发架构 转变成这样的架构 我们把 UI 视图和模型设为 在所谓的“主 Actor”上运行 而后台操作则会在 专用 Actor 上执行 Actor 使用线程安全的值类型 并借助 Swift 的 async/await 功能 与彼此通信 完成重构后 这个并发架构 便与类型架构一样 清晰有序且易于描述 但这里有一个小问题 通过这个重构操作 来改进架构时 我作为程序员 仍需担负很多责任 以免发生数据争用 我遵循了所有指导原则 还使用了值类型 在 Actor 之间进行通信 而我本来不必这样做 举例来说 我本可以使用 引用类型 例如类 将它从一个 Actor 传递给另一个 Actor 引用类型允许四处传递 共享可变状态 但这种做法 会破坏 Actor 提供的互斥性 允许这两个 Actor 同时访问 共享状态 所以 如果我将一个类实例 从一个 Actor 发送到另一个 Actor 仍可能会产生数据争用 这可能会导致程序崩溃 更严重的是 可能会破坏用户数据 这就引出了 Swift 6 的好处
Swift 6 语言模式引入了 全面的数据隔离强制执行 编译器可防止 在不同任务和 Actor 之间 出现这类意外的状态共享 从而允许你执行重构 或向你的 App 添加新功能 而无需担心引入 新的并发错误
Swift 6 语言模式同时适用于 现有项目和新项目 通过在编译时 捕获并发代码中的错误 采用这个模式 可大幅提高 App 质量 当你遇到难以重现的崩溃问题 但是又想系统性地 彻底消除 数据争用风险时 这个模式便尤其有用 如果你正致力于 集成更多并发 以提升响应能力和性能 采用 Swift 6 模式 则可确保这些变化 不会引发新的数据争用风险
如果你负责维护公共 Swift 软件包 则建议你尽快采用 Swift 6 以便为也想迁移代码库的用户 提供帮助 这些用户将能够使用 同样采用了 Swift 6 的依赖项 更好地进行构建 大家可以访问 swiftpackageindex.com 跟进查看热门软件包 对 Swift 6 的采用情况 今天 我们来看看 迁移到 Swift 6 的实际过程 为此 我们将使用 CoffeeTracker 应用程序 并启用 Swift 的数据隔离功能 我们将分步完成这个操作 并看看编译器所提供的 部分指导 了解何时需进行修改 从而让 Swift 保证 CoffeeTracker 不出现 任何数据争用问题 眼下 我并不认为我的 App 真的存在数据争用问题 而你的代码 很可能也是如此 多年来 你可能已通过 不断优化、检查错误报告、 使用主线程检查器 和线程错误检测器等工具 从现有代码中消除了 大多数数据争用问题 数据争用安全功能的实际价值在于 避免所编写的新代码中 出现错误 无论你是为了添加新功能 还是重构现有代码 以更好地利用并发功能 借助数据争用安全功能 你可在 App 中充分利用并发 而无需担心引入新的数据争用 由于无法重现崩溃场景 这类争用问题常需进行后续追踪 或是为它开展推测性修复
自从上次公布之后 我们的 咖啡饮用量追踪 App 已有很大发展 同时我们也已扩充团队 以便开始添加新的功能
为此 我们已将这个 App 的某些功能 合并到新的 CoffeeKit 框架中 而某些代码现已完成迁移 我们的团队十分渴望 向这个 App 添加新功能 但在这之前 我们需将它更新到 Swift 6 这样便可确保 我们在添加新功能时 不会引入新的并发错误 我刚刚已下载 Xcode 16 并将它打开 我们有了新的 Swift 6 编译器 以及适用于 watchOS 11 的最新 SDK 下面来尝试构建 App
整个构建过程非常顺利 无需进行任何更新 但这并不是因为我们的 App 没有任何潜在的数据争用问题 而是因为我们尚未启用 Swift 6 语言模式 与以往的版本一样 Swift 6 提供源代码 兼容保证 除了些许变化 应始终使用新的编译器 来构建 App
既然现在已知我们的 App 是用最新 Xcode 构建的 我们还想更进一步 来启用 Swift 6 模式 从而实现全面的 数据隔离强制执行 现在 作为准备工作 你可先 尝试执行 MainActor 和 Sendable 审核 然后再启用任意编译器诊断 但这样做会错失 新 Swift 编译器带来的好处 编译器诊断可引导你 关注需要修复的位置 你可以把它想象为一名结对程序员 可帮你指出代码中的 潜在错误 这会让迁移流程 更有条理 接下来 我们将完成 一个分步式流程 在这期间 我们会迁移 代码中的每一个目标 对于每个目标 我们均会完成以下步骤: 首先 启用 完整并发检查 这是一种按模块设置 可让项目保持 Swift 5 模式 但同时会对所有未通过 Swift 6 强制数据隔离检查 的代码启用警告 于是 我们需要通查一遍 解决针对这个目标的 所有警告
完成这个操作后 我们接着便会启用 Swift 6 模式 这会锁定我们做出的所有更改 并防止任何未来的重构操作 回退到不安全的状态 接着 我们转到下一目标 并重复执行这个流程 最后 启用这个功能后 我们可能想回过头来 执行一些全 App 重构操作 比如通过更改架构来 撤销某些不安全的选择退出操作 或者进行某些你先前发现 可优化代码的重构操作 关于重构 我们有一点建议: 尽量避免同时 进行大规模重构 和实现数据争用安全 一次仅尝试执行一个操作 如果你想同时执行这两项操作 则可能会发现 一次会出现太多变化 于是不得不进行回溯
本次讲座将仅关注 在 App 中启用 Swift 6 的步骤 这个 App 先前已进行重构 可使用 Swift 并发功能 前面说到 我们首先要启用 完整检查功能 完整检查有什么作用? 如果你在 App 中使用过 Swift 并发 则可能已见过 Swift 编译器发出的 警告或错误 其中说明了采用 Swift 并发功能时 出现的并发问题 例如 我们要 添加一个新的委托 这个委托会收到 我们为 CoffeeKit 添加的回调 以便在我的咖啡因水平 极低时告知我 这个委托会将一个值 发布回 SwiftUI 视图 因此我希望 把它限制在 Main Actor 上 于是我可在顶部添加 @MainActor
从而要求所有方法 和属性访问 均在主线程上执行 但在我执行这个操作时 我在代码下方遇到一个错误 其中涉及协议的实现
这个错误显示 “Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement” 这个 CaffeineThresholdDelegate 协议
现在并未对调用方式 提供保证 它包含在 CoffeeKit 框架中 这个框架尚未更新到 Swift 6 但这里 我尝试让 Recaffeinater 类型遵从这一协议 刚刚 我已将它 限定于主 Actor 由于它的方法会在 主 Actor 上运行 因此 它们并不能遵从 这样一个无法始终保证 会在主 Actor 上调用的协议 稍后我们会再次回到这个问题 并尽快解决它 但这是 Swift 编译器 所生成错误的一个示例 这个错误是因为 你选择让某一类型接受检查 来确认它的调用是否 发生在主 Actor 上 如果你看过先前的讲座 也就是 介绍如何在 CoffeeTracker 中 采用 Swift 并发的讲座 那么你可能已经知道 采用并发时 App 的不同位置 会出现这些问题
通过在目标的构建设置中 启用严格检查 你可以选择让整个模块接受检查 看看是否存在潜在的争用情况 现在 我们来启用这个功能 看看接下来会怎样 Swift 中的数据隔离功能 将按每个目标分别启用 而在我的 App 中有两个主要目标
一个是 WatchKit 扩展 也就是 UI 层所在的位置 另一个就是 CoffeeKit 它是业务逻辑所在的框架 这个逻辑用于追踪咖啡因数据 并将数据存储到 HealthKit 中
首先 我会在 Watch 扩展上 启用完整检查功能 这样做出于两个原因 首先 在这里启用并发检查 通常更为简便 UI 层的大部分任务 均会在主线程上运行 同时还会使用 SwiftUI 或 UIKit 等 API 这些 API 可自行保证 在主线程上执行操作 另一个原因则是 启用严格并发时 你通常还需使用 尚未针对 Swift 并发 进行更新的其他模块 或许你用的是 永不会更新的 C 语言库 或是未来会更新到 Swift 6 但目前尚未更新的 某一框架或软件包模块 其中当然还有 我们自己的 CoffeeKit 框架 等我们开始后 大家就会明白为何 采用这个自上而下的方法会有帮助 首先 找到这个扩展 然后找到相关的设置 接着 搜索 Swift 并发检查设置
将检查方式设为 Complete
完成这个操作后 编译器会立即开始 针对无法确认并发安全的代码 发出警告 这只是一些警告 并不会中断项目的构建和运行 那么 我们来尝试进行构建
现在 除了先前提到的那一个警告 我们还会看到更多警告 我们来看看这些警告
现在 我们看到的第一个问题 属于在 Swift 6 中构建时 最常见的问题之一 涉及“logger”变量
我们看到 有一个 logger 实例 声明为全局变量 全局变量是 共享可变状态的源头 程序中的每一段代码 无论它在哪个线程上运行 都能对这个变量 进行读写 所以它极易成为 数据争用的源头 因此我们需要 确保这个变量的并发安全 我们有几种处理方式可供选择 事实上 如果我们展开这个问题
编译器便会推荐一些选项
第一个选项最为简单 我们只需将它设为只读即可 logger 是一种 Sendable 类型 因此 如果将它 声明为 let 变量 那么在多个线程中使用这个变量时 就不会导致数据争用问题 于是 我们将 var 切换为 let 然后进行重建
这个问题便可迎刃而解 这个解决方法还不错 但在这里 我们还可以选择其他选项
假设我不想让这个值成为不可变值 后续还想更新这个值 所以需要让它保持为 var 变量 而不是 let
另一个选项是将这个全局变量 与全局 Actor 相关联 这里是 UI 层 或许所有日志记录操作 都会在主 Actor 上执行 于是可将这个全局变量 标注为 @MainActor
没错 我的所有日志记录用途 都源自主 Actor 因此这个方法也可消除警告
最后 有时我还会使用一些别的 外部机制来 保护这个变量 而编译器无法强制执行这些机制 比如 我在使用调度队列 来保护所有访问 这时便可使用 nonisolated(unsafe) 关键字
就像在 Swift 中使用 “unsafe”一词的其他情况一样 使用这种关键字会将确保 这个变量安全的责任交给你 只有在万不得已时 才应该使用这种方法 最好还是使用 Swift 的 编译时保证 但编译器无法 知晓一切 因此在这类情况下 你可以 选择使用 nonisolated(unsafe) 这可能意味着 后续我需要回过头来 重构这段代码 也许会将这个变量 移入某个 Actor 从而让编译器能够验证 变量的使用足够安全 但现在 我可将它标记为 nonisolated(unsafe) 然后继续处理下一个警告 由于本例不属于那些适用的情况 因此我要改回刚才的做法 将这个变量声明为 let
因为这才是目前最合适的方法 在这里 你可能希望了解 这个用于
初始化全局变量的构造器 它会何时运行? 这一点对于理解线程安全 非常重要 不是吗?
Swift 中的全局变量 采用延迟初始化 在首次使用时 对值进行初始化 也就是 CoffeeTracker 首次记录某些信息时 与 C 语言和 Objective-C 语言相比 这是一个非常重要的区别 在这些语言中 全局变量会在 启动时进行初始化 而这可能会严重影响 启动时间 Swift 的延迟初始化 可避免这些问题 以便你的 App 更快进入可用状态 但是延迟初始化 也会引发争用问题: 如果有两个线程 同时尝试使用这个全局变量 来进行首次记录 那会怎样? 这会导致生成两个 logger 吗? 别担心 在 Swift 中 全局变量可保证 以原子方式进行创建 如果两个线程 同时尝试 首次访问一个 Logger 则只有一个线程会对它初始化 而另一个则会被阻而需等待 这样 这个问题就解决了 接下来我们来看看其他问题
这里有一些代码用于访问 WKApplication 的共享实例 这个全局实例方法是 限定在主 Actor 上的方法的示例
在这里 第一个备注表示 对 Actor 隔离状态的调用 本质上是隐式异步调用 换言之 如果这是一个 async 函数 则可使用 await 在主 Actor 上访问这个全局变量 但这个函数现在并非异步函数 因此需要将它标记为 async 或启动新的任务 但编译器还为我们 提供了另一种解决方案
我们可以直接将这个函数 置于主 Actor 上执行
由于它是一个自由函数 而不是视图的方法 因此它不会默认 在主 Actor 上执行 现在 我们来应用这种解决办法
现在 这个方法便会 限定在主 Actor 上 我们来尝试进行构建 构建成功了
要了解这个方法为何有效 我们可快速查看一下调用方
我们可以看到 有两次调用 而这两次调用都是从已知位于 主 Actor 中的方法来执行的 如果这个函数是在主 Actor 之外的位置进行调用的 则会出现一个显示这个问题的 编译器错误 而我可以查看相应的调用方 确定调用时所在的上下文 在这里 一个调用方 是 SwiftUI 视图 另一个则位于 WKApplicationDelegate 实现中 现在我们来具体看看
如果我们按住 Option 键 并点按 WKApplicationDelegate 就可以看到它是一个 与主 Actor 关联的协议 这样就能保证这个协议 只会在主 Actor 上进行调用 而调用方要么是 WatchKit 框架 要么是在启用 Swift 6 模式后 由你的代码调用
很多委托和其他协议 比如 SwiftUI 视图 均可仅在主 Actor 上运行 而相关注释也是这么定义的 尤其是在 Xcode 16 随附的最新 SDK 中 其中最重要的是 它包括了 SwiftUI View 协议 你可能会发现 如果先前启用了严格并发检查 则须添加更多主 Actor 注释 比新 SDK 规定的注释还要多 而且你还会发现 或许能够 删除其中某些注释 现在 我们来聊聊 委托回调和并发 你可能已知道 无论何时从委托 或完成处理程序收到回调 均须首先了解 这个回调有哪些 并发保证 某些回调有一个保证 而相关文档中可能会写到 所有回调均须 始终在主线程中执行 很多 UI 框架 都提供这种保证 正是由于这一点 以及其他原因 开发 Watch 扩展的 这一视图层 将对象标记为主 Actor 时 我们并没有遇到太多警告
另一方面 某些委托 的做法正好相反 它们不会保证 自身的回调方式 而是表示它会在 某个任意线程或队列中执行 而这对于更有可能会进入 App 后端的回调 非常重要 CoffeeTracker 从 HealthKit 收到的回调就属于这种情况 在这类情况下 用户需 重新调度到正确的队列或 Actor 或是以线程安全的方式进行构建 这个方法的问题在于 由于其中每个委托都有自己的规则 而这些规则只会在文档中列出 这会给用户 也就是你 带来很大压力 从而无法顺利构建 你需要考虑 回调时所处的位置 以及需要到达哪个位置 才能继续执行逻辑的下一部分 假如你忘了检查 或是忘了重新调度到 正确的位置 则极易出现数据争用 更糟糕的是 假设回调 已经开始且在运行 而它恰好始终是在 主队列中执行 但这种情况并不总能得到保证 而后续 这个框架还会出现某些变化 于是会转到 另一个队列去执行 但依赖它的 UI 层 又位于主队列中 这里我们缺乏的是 本地推理能力 它可以保证当我 专注于 UI 层时 这层的工作不会被我 App 中 其他位置的代码更改轻易影响到 比如更改执行操作所在的队列
Swift 并发可解决这个问题 将这些保证 或缺少保证的情况显式化 如果某一回调未指定 回调方式 则会将它视为非隔离 而且也无法访问 需要特定隔离级别的数据 另一方面 如果某一回调 确实提供了隔离保证 并表示它会始终 在主 Actor 上进行回调 则它可将 委托协议或回调 标注为始终 在主 Actor 上执行 且这个回调的接收方 可依赖这个保证 现在 让我们回到 我们看到的第一个警告 其中表示主 Actor 的委托类型 无法遵从非隔离协议 这里 编译器提供了 几个选项 第一个选项是 将这个方法声明为非隔离 也就是说 虽然这个方法所在的类型 限定在主 Actor 上 但这个特定方法 不会限定在主 Actor 上 对于故意不对回调位置 做出承诺的回调 就应采用这个方法 当然 由于它是一个视图 我需要立即 转到主 Actor 进行一些处理 如果不这样做 我便会在编译这段代码时 收到新的错误消息 因为我正在访问 这个视图的属性 而这些属性受到主 Actor 的保护
通过在主 Actor 上启动一个任务 我可以修复这个问题
但在本例中 我知道这个回调 应在主 Actor 中执行 我还知道它来自 CoffeeKit 中的模型类型 这个模型类型本身 就是在主 Actor 上运行
如果我负责维护整个代码库 有一个选项则是 立即想办法解决这个问题 我可以跳到定义中 接着 我可在 CoffeeKit 中看到这个协议 我可在这里用 @MainActor 对它进行标注 以保证它会在 主 Actor 上进行调用 但有时 你并不需要用这种做法 来维护整个代码库 比如有其他团队在维护 CoffeeKit 或是你依赖的某个软件包 或框架由他人负责维护 假设这里便是这种情况 然后我们返回到 委托实现
现在 我知道这个方法 会在主 Actor 上进行调用 而我刚刚检查了代码 或是我在有关这个委托的 某些文档中读到过它
当你确定某个调用 会在特定 Actor 上执行时 便可使用某一方法 向编译器告知这个情况 这种方法就是 assumeIsolated
因此 我不再编写 用于启动任务的代码 而是写下 MainActor.assumeIsolated 它不会启动新任务 以异步传输到主 Actor 上 而只是告知 Swift 这个代码 已在主 Actor 上运行
请注意 这个代码的 某些部分依然很可能会 从主 Actor 之外的位置 来调用这个函数 这与当前的 Swift 函数一样 因为它们均假定自己 会从主线程进行调用 防止这个情况的一个好办法是 在确实位于主 Actor 的函数内 添加一个断言 这也正是 assumeIsolated 的作用 如果这个函数 从未通过主 Actor 进行调用 它便会进行错误捕获 并导致你的程序停止运行 你不希望触发错误捕获 但它总比可能损坏用户数据 的争用情况要好
如今 这种遵从某一委托协议 从而假定它会在 主 Actor 上进行调用的模式 已十分常见 因此它有一个简化形式 下面我先撤消我做的更改
然后换种做法 针对协议遵从性 添加 @preconcurrency
这样便可完成 通过手写代码实现的所有任务 它会假设自己正在这个视图 限定的 Actor 上进行调用 否则便会触发错误捕获
消除完 所有并发警告后 我们便可在这个目标中 启用 Swift 6 于是 我们转到设置部分 这次改为搜索 Swift Language Mode
然后将它设为 Swift 6
进行编译 然后构建时就不会出现 任何错误和警告 现在 我已在扩展中锁定 全面数据隔离检查 我在这里做出的所有后续更改 均会通过编译器 进行全面数据隔离检查 从而确保我不会 不慎引入数据争用问题 那么 既然扩展 已采用 Swift 6 模式 我们便可将注意力 转向 CoffeeKit 目标 由于我们要处理这个目标 于是我们将 @MainActor 注释添加到委托协议中 然后我们要找到它 添加主 Actor 注释 进行重建 接着会出现新的警告
这个警告又在扩展中出现了 而它涉及我们刚刚添加的 @preconcurrency 属性
由于编译器知道 这个协议可保证 在主 Actor 上运行 因此编译器会发出警告 表示已不再需要 这个 @preconcurrency 属性 于是我们可将它删除
好了 解决这个问题后 我们可按照与以前相同的例程 来启用完整并发检查 我们来转到项目设置部分
为 CoffeeKit 目标 启用完整检查
接着进行构建
现在 我们能看到更多警告 共有 11 条 对于一个非常简单的项目来说 这警告数量可算不少了
这种情况非常普遍 当你在项目中 启用完整并发检查后 你的项目会生成数百个 甚至数千个警告 出现这个情况时 你可能会担心 自己是不是设置错了 这时 一定不要惊慌
少数几个问题导致大量警告 这并不奇怪 其中很多问题 通常都能快速修复
因此 在首次清理 并发警告时 一系列简单的修改 比如为主 Actor 设置一个方法 或是将某一全局变量改为不可变 即可快速 减少这类警告 建议在首次开启严格检查时 采取这类速效措施并进行处理 以便先用最简单的修复方法 来减少这类警告 此外 还应格外留意可能引发 大量其他问题 的根源问题 有时 改动一行代码 就能解决数百个连锁问题 如果你已在先前版本的 Xcode 上 试用完整检查模式 则还应在最新的 Xcode Beta 版上进行试用 较新的 SDK 包含更多注释 来帮助完成迁移 例如 所有 SwiftUI 视图 现已全部关联到主 Actor 因而无需再 手动向视图类型 添加 MainActor 注释 事实上 你可能会发现 现在可移除其中某些注释 因为它们现在已可推断出来
完成这个操作后 你会发现 更难处理的警告 会大幅减少 另外请牢记 你现在无需一次性 解决所有这些问题 如须发布某一版本 或处理某些较为紧迫的更改 你可返回到设置界面 然后重新关闭严格检查 你为减少这类警告而 做出的所有更改 均会有效改善 你可维护和签入 的代码库 即便你稍后 还需进行少量检查 你可在准备就绪后回到那个设置界面 启用检查 然后再解决相关问题
对于我们所处的情况 当我们开始查看这些警告时 我们会发现一种先前见过的模式 其中几个全局变量 被标记为 var 但我认为它们都是常量 或无需设为“可变” 就像我们之前看到的 logger 一样 我们可以快速解决 所有这些问题 顺便说一句 这是尝试使用 多行编辑技巧的好机会
只需先把 var 全部高亮显示
按下 Command-Option-E 把这些变量全选中
将它们更改为 let
完成构建
这样即可消除这些警告
我不想让你觉得 我故意简化了问题 这只是一个示例项目 在实际项目中 你会看到更多警告 但根据我们的经验 说到真正的大规模项目 这种情况会非常普遍 既有很多便捷措施可供使用 也会出现一些较难解决的问题 接下来 我们看看最后一些错误 这些错误是因为 我们 在不同 Actor 之间 传递了 Drink 数组 例如 第一个错误表示 将 self.currentDrinks 发送到 save 方法 可能会导致数据争用 请注意 save 是属于 另一个 Actor 的方法 CoffeeData 属于主 Actor 原因在于它是一个 SwiftUI ObservableObject
但 save 属于另一个 Actor
也就是这个 CoffeeDataStore Actor 这个 Actor 负责在后台 从磁盘进行存储和载入 如果我们回到这个警告 就会发现我们发送到 save 的 Drink 数组 与模型中限定于主 Actor 上的 Drink 数组是一样的
如果 Drink 是一个引用类型 我们则会引发潜在的数据争用 这时主 Actor 以及存储 Actor 均可能同时有权访问 共享的可变状态 为解决这个问题 我们来看看 Drink 如果我们转到定义中
则会看到它是一个结构体 其中附带几个不可变属性 且全部为值类型 基于这一点 显然可将它 设为 Sendable 然后在一个 Actor 中保存 这些 Drink 就不会有任何问题 接着我们把同一数组 发送给另一个 Actor
这时 如果它是一个内部类型 Swift 会自动帮你将这个类型 视为 Sendable 但由于它是一个公共类型 因此需在 CoffeeKit 之外 与 CoffeTracker 扩展进行共享
Swift 不会自动为公共类型 推断可发送性 这是因为 将某一类型标记为可发送 是为客户端做出的一项保证 这个类型目前不含任何可变状态 但或许将来我想改变这个状况 我不想过早 锁定可发送性 为此 Swift 要求你 对公共类型显式添加 Sendable 遵从性
在这个情况下 我很愿意手动添加 恰巧 这是一个 通过更改一行代码便可 一次性消除多个警告的示例: 我们需在三个地方将 Drink 类型设为 Sendable 在大型项目中 可能不止三个 也许会在多个项目中 收到数十个警告 我们继续 将这个类型标记为 Sendable
我们可重新进行编译
然后我们能看到这里 还有一个不可发送的类型
这时 由于它恰好是一个枚举 于是我也可将它标记为可发送 但如果不是这样呢 如果它是 Objective-C 类型又该如何? 或许有一个类型绝不可发送 因为它会将可变状态 存储在引用类型中 这时 你可能需要就安全性 做出一些选择 其中一个选择是 推断这个类型并加以确定 即使它是一个可变引用类型 这样做很安全 因为它是一个 可能由 NSCopying 生成的全新副本 你可能会发现 即便它不是可发送类型 你也能保护这个类 比如 将这个变量设为 private 并仍将它储存为可发送类型 为此 你可再次使用 nonisolated(unsafe) 关键字
如果确有使用 Drink 类型这时便可 用 Sendable 注释进行编译 眼下 我知道它并非必要操作 于是我会将它撤消
然后转到 DrinkType
并将它也标记为 Sendable
在 CoffeeKit 中消除完 所有并发警告后 我们便可在这个目标中 启用 Swift 6 模式 于是 我们再次转到设置部分
这次改为搜索 Swift Language Mode
然后将它设为 Swift 6
进行编译
然后完成构建 这时 整个 CoffeeTracker App 便受到 Swift 并发的保护
最后 由于我们已实现保护 我们继续了解一下如何 向 CoffeeTracker 添加新功能 我们的用户想要开始追踪 自己喝咖啡的位置 通过挖掘这些数据 获取有关自己 咖啡因摄入习惯的关键洞察 我们来看看如何将 CoreLocation 添加到 App 中 为此 我们转到 CoffeeKit 中的 addDrink 方法 在开始添加 Drink 之前 我们将使用 CoreLocation 来获取用户的当前位置 CoreLocation 有一个异步序列 它会流式传输当前位置 可以很好地兼容 Swift 并发功能 我们可以在这里使用它来 循环遍历位置结果 直到实现 所需的准确度水平 而达到这个水平后 便可将位置分配给咖啡因样本 你可能想为这个代码 添加一个超时时间 为此 你可以使用 Swift 的结构化并发 与取消功能 这个方法 只有一个问题
那便是 我们必须提高 CoffeeTracker 的最低部署目标 而我们还没有为此做好准备 因为仍有些用户 不想更新到 watchOS 10 但又很想跟踪 他们喝咖啡的位置 因此 我们需使用 基于委托回调的 旧版 CoreLocation API 由于它们都出现于 Swift 并发之前 因此用起来会有点棘手 但我们在这里看到的方法 有一点很好 那便是 这个代码看起来 就像常规的同步代码 我们可在同一函数中查询位置 循环遍历位置更新的传入流 直到找到一个 足够精准的位置为止 到时我们就会 向数组添加一个新 Drink 相比之下 委托 API 要求我们储存某些状态 等待委托方法触发 然后继续 用一个位置 存储我们的 Drink 值 因此 你有必要尝试 提高部署目标 以便充分利用这些新的 API
但假设我们不想那样做 那么我们可以删除这段新代码
向下转到这个文件的底部
接着创建一个 CoreLocation 委托对象
这样 我便完成了一个 委托类的基本实现 而它可从 CoreLocation 接收位置更新 这部分 CoreLocation 出现于 Swift 并发之前 因此我必须完成某些附加操作 来确保遵守相关规则 与我们在本次讲座中看到的 所有其他回调不同 这个 CLLocation 委托 略有不同 它无法静态保证 会在哪个线程上进行回调
到目前为止 我们谈的都是 始终会在主线程上调用的委托 或是会在任意线程上 调用的委托 如果你查看 CoreLocationManager 的文档 会发现它提供的保证是 调用这个委托的线程 取决于 你创建了 CLManager 的那个线程 这是一种动态属性 Swift 无法为你自动强制应用 除非有一些额外的辅助
在本例中 我们是在模型类型中 使用来自主 Actor 的这些信息 因此 最简单的方法是确保 同样会在主线程上 调用这个委托 要回到 Swift 能够帮我们 强制实现这一点的模式很简单 我们可将这个类型 完全置于主 Actor 上
这意味着会在主线程中 创建一个位置管理器
也就是在这里的构造器内 所以 这个委托 也会在主 Actor 上执行
当然 一旦完成这个操作 我们便会收到来自编译器的 那则熟悉的错误 告知我们这个委托回调 未限定在主 Actor 上 我们已了解有关 如何处理这个情况的模式
我们可将这个委托 标记为 nonisolated
然后将必须在主 Actor 上 运行的代码 打包到 MainActor.assumeIsolated 调用中
现在 我的构建已成功完成 我们的 App 已经 可以收集当前位置了 且依然是在 Swift 6 中进行构建 这里简单概括了 将 App 迁移到 Swift 6 语言模式 的一些技巧 还有很多场景 今天并未介绍 但我们有更多资源 可供你使用 你可首先观看 之前的讲座 其中介绍了如何使用 Swift 的并发功能 对现有 App 进行现代化改造 借助本次讲座所介绍的 某些重构方法 你可以更轻松地完成将这些代码 迁移到 Swift 6 的大部分工作 根据以往迁移工作的经验教训 Swift 6 语言模式适用于 跨 Swift 生态系统的 增量迁移 所以 一次进行一项代码修改 可逐步消除数据争用问题 我们只是初步探讨了 在开发者社区开始共同迁移的过程中 将静态数据争用安全 与动态数据争用安全 结合起来的一些技巧 你可在 Swift.org/migration 上 找到包含所有这类策略 及更多信息的指南 我们希望这些资源能在你 构建和优化自己的代码时提供帮助 谢谢观看!
-
-
9:08 - Recaffeinater and CaffeineThresholdDelegate
//Define Recaffeinator class class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //Add protocol to notify if caffeine level is dangerously low extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
9:26 - Add @MainActor to isolate the Recaffeinator
//Isolate the Recaffeinater class to the main actor @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 }
-
9:38 - Warning in the protocol implementation
//warning: Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } }
-
9:59 - Understanding why the warning is there
//This class is guaranteed on the main actor... @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //...but this protocol is not extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
12:59 - A warning on the logger variable
//var 'logger' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
13:38 - Option 1: Convert 'logger' to a 'let' constant
//Option 1: Convert 'logger' to a 'let' constant to make 'Sendable' shared state immutable let logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
14:20 - Option 2: Isolate 'logger' it to the main actor
//Option 2: Annotate 'logger' with '@MainActor' if property should only be accessed from the main actor @MainActor var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
14:58 - Option 3: Mark it nonisolated(unsafe)
//Option 3: Disable concurrency-safety checks if accesses are protected by an external synchronization mechanism nonisolated(unsafe) var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
15:43 - The right answer
//Option 1: Convert 'logger' to a 'let' constant to make 'Sendable' shared state immutable let logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
17:03 - scheduleBackgroundRefreshTasks() has two warnings
func scheduleBackgroundRefreshTasks() { scheduleLogger.debug("Scheduling a background task.") // Get the shared extension object. let watchExtension = WKApplication.shared() //warning: Call to main actor-isolated class method 'shared()' in a synchronous nonisolated context // If there is a complication on the watch face, the app should get at least four // updates an hour. So calculate a target date 15 minutes in the future. let targetDate = Date().addingTimeInterval(15.0 * 60.0) // Schedule the background refresh task. watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { //warning: Call to main actor-isolated instance method 'scheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:)' in a synchronous nonisolated context error in // Check for errors. if let error { scheduleLogger.error( "An error occurred while scheduling a background refresh task: \(error.localizedDescription)" ) return } scheduleLogger.debug("Task scheduled!") } }
-
17:57 - Annotate function with @MainActor
@MainActor func scheduleBackgroundRefreshTasks() { scheduleLogger.debug("Scheduling a background task.") // Get the shared extension object. let watchExtension = WKApplication.shared() // If there is a complication on the watch face, the app should get at least four // updates an hour. So calculate a target date 15 minutes in the future. let targetDate = Date().addingTimeInterval(15.0 * 60.0) // Schedule the background refresh task. watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { error in // Check for errors. if let error { scheduleLogger.error( "An error occurred while scheduling a background refresh task: \(error.localizedDescription)" ) return } scheduleLogger.debug("Task scheduled!") } }
-
22:15 - Revisiting the Recaffeinater
//This class is guaranteed on the main actor... @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //...but this protocol is not //warning: Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
22:26 - Option 1: Mark function as nonisolated
//error: Main actor-isolated property 'minimumCaffeine' can not be referenced from a non-isolated context nonisolated public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } }
-
23:07 - Option 1b: Wrap functionality in a Task
nonisolated public func caffeineLevel(at level: Double) { Task { @MainActor in if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
23:34 - Option 1c: Explore options to update the protocol
public protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) }
-
24:15 - Option 1d: Instead of wrapping it in a Task, use `MainActor.assumeisolated`
nonisolated public func caffeineLevel(at level: Double) { MainActor.assumeIsolated { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
25:21 - `@preconcurrency` as a shorthand for assumeIsolated
extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
26:42 - Add `@MainActor` to the delegate protocol in CoffeeKit
@MainActor public protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) }
-
26:50 - A new warning
//warning: @preconcurrency attribute on conformance to 'CaffeineThresholdDelegate' has no effect extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
27:09 - Remove @preconcurrency
extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
29:56 - Global variables in CoffeeKit are marked as `var`
//warning: Var 'hkLogger' is not concurrency-safe because it is non-isolated global shared mutable state private var hkLogger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.HealthKitController", category: "HealthKit") // The key used to save and load anchor objects from user defaults. //warning: Var 'anchorKey' is not concurrency-safe because it is non-isolated global shared mutable state private var anchorKey = "anchorKey" // The HealthKit store. // warning: Var 'store' is not concurrency-safe because it is non-isolated global shared mutable state private var store = HKHealthStore() // warning: Var 'isAvailable' is not concurrency-safe because it is non-isolated global shared mutable state private var isAvailable = HKHealthStore.isHealthDataAvailable() // Caffeine types, used to read and write caffeine samples. // warning: Var 'caffeineType' is not concurrency-safe because it is non-isolated global shared mutable state private var caffeineType = HKObjectType.quantityType(forIdentifier: .dietaryCaffeine)! // warning: Var 'types' is not concurrency-safe because it is non-isolated global shared mutable state private var types: Set<HKSampleType> = [caffeineType] // Milligram units. // warning: Var 'miligrams' is not concurrency-safe because it is non-isolated global shared mutable state internal var miligrams = HKUnit.gramUnit(with: .milli)
-
30:19 - Change all global variables to `let`
private let hkLogger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.HealthKitController", category: "HealthKit") // The key used to save and load anchor objects from user defaults. private let anchorKey = "anchorKey" // The HealthKit store. private let store = HKHealthStore() private let isAvailable = HKHealthStore.isHealthDataAvailable() // Caffeine types, used to read and write caffeine samples. private let caffeineType = HKObjectType.quantityType(forIdentifier: .dietaryCaffeine)! private let types: Set<HKSampleType> = [caffeineType] // Milligram units. internal let miligrams = HKUnit.gramUnit(with: .milli)
-
30:38 - Warning 1: Sending arrays in `drinksUpdated()`
// warning: Sending 'self.currentDrinks' risks causing data races // Sending main actor-isolated 'self.currentDrinks' to actor-isolated instance method 'save' risks causing data races between actor-isolated and main actor-isolated uses await store.save(currentDrinks)
-
32:04 - Looking at Drink struct
// The record of a single drink. public struct Drink: Hashable, Codable { // The amount of caffeine in the drink. public let mgCaffeine: Double // The date when the drink was consumed. public let date: Date // A globally unique identifier for the drink. public let uuid: UUID public let type: DrinkType? public var latitude, longitude: Double? // The drink initializer. public init(type: DrinkType, onDate date: Date, uuid: UUID = UUID()) { self.mgCaffeine = type.mgCaffeinePerServing self.date = date self.uuid = uuid self.type = type } internal init(from sample: HKQuantitySample) { self.mgCaffeine = sample.quantity.doubleValue(for: miligrams) self.date = sample.startDate self.uuid = sample.uuid self.type = nil } // Calculate the amount of caffeine remaining at the provided time, // based on a 5-hour half life. public func caffeineRemaining(at targetDate: Date) -> Double { // Calculate the number of half-life time periods (5-hour increments) let intervals = targetDate.timeIntervalSince(date) / (60.0 * 60.0 * 5.0) return mgCaffeine * pow(0.5, intervals) } }
-
33:29 - Mark `Drink` struct as Sendable
// The record of a single drink. public struct Drink: Hashable, Codable, Sendable { //... }
-
33:35 - Another type that isn't Sendable
// warning: Stored property 'type' of 'Sendable'-conforming struct 'Drink' has non-sendable type 'DrinkType?' public let type: DrinkType?
-
34:28 - Using nonisolated(unsafe)
nonisolated(unsafe) public let type: DrinkType?
-
34:45 - Undo that change
public let type: DrinkType?
-
35:04 - Change DrinkType to be Sendable
// Define the types of drinks supported by Coffee Tracker. public enum DrinkType: Int, CaseIterable, Identifiable, Codable, Sendable { //... }
-
36:35 - CoreLocation using AsyncSequence
//Create a new drink to add to the array. var drink = Drink(type: type, onDate: date) do { //error: 'CLLocationUpdate' is only available in watchOS 10.0 or newer for try await update in CLLocationUpdate.liveUpdates() { guard let coord = update.location else { logger.info( "Update received but no location, \(update.location)") break } drink.latitude = coord.coordinate.latitude drink.longitude = coord.coordinate.longitude } catch { }
-
38:10 - Create a CoffeeLocationDelegate
class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? var manager: CLLocationManager! var latitude: CLLocationDegrees? { location?.coordinate.latitude } var longitude: CLLocationDegrees? { location?.coordinate.longitude } override init () { super.init() manager = CLLocationManager() manager.delegate = self manager.startUpdatingLocation() } func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.location = locations. last } }
-
39:32 - Put the CoffeeLocationDelegate on the main actor
@MainActor class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? var manager: CLLocationManager! var latitude: CLLocationDegrees? { location?.coordinate.latitude } var longitude: CLLocationDegrees? { location?.coordinate.longitude } override init () { super.init() // This CLLocationManager will be initialized on the main thread manager = CLLocationManager() manager.delegate = self manager.startUpdatingLocation() } // error: Main actor-isolated instance method 'locationManager_:didUpdateLocations:)' cannot be used to satisfy nonisolated protocol requirement func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.location = locations. last } }
-
40:06 - Update the locationManager function
nonisolated func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { MainActor.assumeIsolated { self.location = locations. last } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。