大多数浏览器和
Developer App 均支持流媒体播放。
-
解密 SwiftUI 性能
了解如何构建 SwiftUI 性能的心智模型,并编写更快、更高效的代码。我们将介绍一些导致性能问题的常见原因,并帮助你解决 SwiftUI 的挂起和卡顿问题,以创建更具响应性的 App 视图。
章节
- 0:00 - Introduction and performance feedback loop
- 3:30 - Dependencies
- 10:48 - Faster view updates
- 13:24 - Identity in List and Table
资源
相关视频
WWDC23
WWDC21
Tech Talks
-
下载
♪ ♪
演讲人:大家好 欢迎来到“揭秘 SwiftUI 性能” SwiftUI 可以轻松构建 复杂且功能强大的 App 并提供大量功能 和复杂的控件 如 List 和 Table 当你刚刚开始开发 并且 App 还不太复杂时 性能问题并不明显 但随着 App 的复杂程度增加 性能变得越来越重要 小的问题可能会被放大 在原型中运行良好的代码 可能会在生产中表现不佳 本次讲座旨在针对 SwiftUI 性能 构建心智模型 因为如果你 在开发过程初期 就了解如何编写快速代码 那么在 App 变得更加复杂时 你遇到的问题也会更少 让我们来看一下解决性能问题 涉及到的反馈循环 性能问题总是从出现一个症状开始 比如导航推送缓慢、动画故障 或者你在 macOS 上 看到了旋转的等待光标 当确定存在性能问题后 解决问题的第一步是进行测量 一旦你进行了测量并确认症状存在 就要寻找其成因 这往往是这个循环中 比较棘手的阶段 因为这要求开发者 保持直觉 了解事物该如何运作 当你的 App 有一个 不正确的假设时 错误就会出现 本次讲座旨在帮助开发者识别 你的 App 的假设和现实的不匹配
在确定了根本原因后 开发者应通过优化来修复问题 但是性能问题并非在你找到根本原因 并优化代码之后就可以解决 开发者需要针对所做的修复 进行重新测量和验证 以确保 问题得以解决 这是适用于所有错误的最佳实践 但这对于性能问题尤为重要 在验证问题得到解决后 你就可以打破这个循环 这张图表将此过程放入了语境中 理想情况下 你永远不会陷入该循环 并且通过在原型设计时 编写快速代码 你可以避免许多性能问题 然而 随着开发者的 App 变得越来越复杂 性能错误不可避免 即使最出色的开发者 也会遇到这些问题 而当你遇到性能问题时 最好能有尽可能多的工具 来处理和修复它们 本次讲座旨在帮你 更轻松地完成这个循环
本次讲座包含进阶内容 需要你提前掌握一些知识 你应该对 SwiftUI 身份 有一个大致的了解 包括隐式身份和显式身份的区别 了解视图生命周期 和视图身份之间的区别也很重要 如果你还不了解这些知识 没关系 你可以先查看 WWDC21 的 “揭秘 SwiftUI” 今天的讲座 将从上次结束的地方继续 让我们来看看本节的议程 首先我们将深入了解依赖关系 并详细探索 SwiftUI 的更新过程 接下来 我们将继续讨论更新 以及如何提高 SwiftUI 更新界面的速度 最后 但同样很重要的是 我们要讨论 List 和 Table 中的身份 在此过程中 我们 还将深入了解 SwiftUI 的 内部情况 并学习一些 开发时使用的技巧和窍门 本次讲座主要关注视图层次结构的 更新缓慢问题 并不会针对开发 App 过程中 可能遇到的所有问题 进行详尽地研究 让我们先从依赖关系开始吧 距离上一次的 “揭秘 SwiftUI”讲座已经过了几年 而我一直想再开发一个 以狗狗为主题的 App 因此我们将延续上次讲座的主题 我一直在开发一款新 App 用于记录我最喜爱的毛茸茸的朋友 并安排一些时间与它们玩耍 这是其中一个视图 一个显示所有狗狗的 Table 这个 App 还提供详情视图 这是它在 iPhone 上的样子 上面展示了每只狗狗 更大的照片、它们的偏好 并提供一个按钮 以点击并安排时间与它们一起玩耍 下面是这该视图的代码 该视图以一只狗作为参数 并具有一个环境属性 用于知道是否到了玩耍时间 正如在之前的揭秘讲座中介绍过的 这意味着狗和玩耍时间的变量 是该视图的依赖项 展示此视图的另一种方式 是用依赖图 这是一张大致表示 相同视图的依赖图 每个箭头代表一个视图主体 狗视图产生了一个 stack 而这个 stack 有多个子项 如一些文本、 可缩放的狗狗图片、 详情视图以及按钮 再往后看 每个视图也有其子项 依赖图会继续扩展 直到该视图不再有子项 比如它是图片、文本或者颜色 所有视图最终都会被分解为 这样的叶视图 SwiftUI 中有很多叶视图 因此我不会在此逐一介绍 请查阅文档以获取更多信息 让我们回到 App 在我使用 App 时 每当我和 狗朋友玩耍后 我都可以记录 比如我刚刚和 Rocky 玩过捡球 所以我在 App 上记录一下 按钮和图片都进行了更新 Rocky 看起来很开心 但是他明显已经累得不能再玩了 当这个数据在模型中发生变化后 SwiftUI 就会更新该视图 让我们回到依赖图 深入了解更新过程 看看变化发生时会发生什么 还是这张图 上次的揭秘讲座就讲到了这里 解释了视图会构成依赖图 以及 SwiftUI 会根据依赖关系 评估开发者的代码 让我们将这张图放大 一起深入了解这些依赖关系的来源 以及该如何控制它们 每个子视图都依赖于其祖先产生的 视图值 但还有其他形式的依赖关系 动态属性也是依赖关系的常见来源 例如 DogView 使用 @Environment 属性包装器 从环境中读取现在是否是玩耍时间 因此 该视图既依赖于 其父视图产生的值 也依赖于环境中的值 如果我们将时间可视化在 X 轴上 更新过程的第一步 就是为视图生成一个新值 这个值包含了视图的所有存储属性 如狗的值和动态属性的初始值 接下来 SwiftUI 更新视图的所有动态属性 用依赖图中当前的属性值替换它们 最后 视图主体用更新后的值运行 以生成视图的子视图 让我们继续来看依赖图 这个过程会通过递归来更新界面 而且只更新那些有了新值 或变更了依赖关系的视图 当我们将 Rocky 标记为玩累了后 我们会得到一只新的狗…… 抱歉 是一个新的狗的 struct 值 但它还是同一只 Rocky 因为我们的数据是值类型 当它发生变化时 会创建一个新的副本 这会导致 DogView 为 stack 生成新的内容 从而更新 stack 的子视图 我只关注这里的 ScalableDogImage 但如果还有其他视图依赖于狗的值 它们也会更新 ScalableDogImage 最终会生成一个新的图像 图像都属于叶视图 所以 SwiftUI 会负责接下来的工作 这个过程就到此结束 并生成新的渲染 这就是查看依赖图的方法 让我们来看看有什么技巧 可以改进这一过程 首先是将更新 减少到仅限必要的部分 如果想知道视图何时更新 SwiftUI 提供 printChanges 方法 你可以使用该方法打印 SwiftUI graph evaluator 调用视图主体的原因 让我们通过一个示例 了解如何使用它 这里有一张可缩放的狗狗图片 其中包含一段状态 当我们点击图片时 状态会发生变化 就像这样
只关注图像视图 如果我们在视图主体中 设置一个断点 我们可以从 LLDB 控制台 使用 “expression” LLDB 命令 来调用 Self._printChanges printChanges 是一个 仅用于调试的工具 它会尽可能解释 SwiftUI 请求视图主体的原因 在本例中 原因是 scaleToFill 发生了变化 开发者可以使用 printChanges 来了解一个视图 是否有额外的依赖关系 比如 我当前正在运行我的 App 并进行调试 我想知道这个视图 是否有额外的依赖关系 我可以为这个视图主体添加一个 printChanges 调用 以便每次视图主体被访问时 进行打印 然而 请注意 printChanges 前面有一个下划线 这种情况意味着 它不能保证始终存在 甚至可能在未来的版本中被移除 所以绝对不要向 App Store 提交对此方法的调用 我一会儿就得把这个调用删除 该方法仅用于调试 并且会对运行时的性能产生影响 如果我重新运行我的 App 并将 Rocky 最喜欢的零食 从饼干改为其他东西 比如说黄瓜 我会在控制台上 看到来自图像的一条日志 上面显示“self”发生了变更 这意味着视图值发生了变更 因此这张可缩放图像肯定 对零食有依赖关系 但它其实并不需要这个依赖关系 我们来看代码 该视图的值只有 scaleToFill 成员和狗的属性 由于 scaleToFill 是 SwiftUI 的动态属性 如果它发生了变化 它会显示在变更日志中 所以这里的“@Self”意味着 狗的值发生了变化 但是在该视图中 我们只关心图像 所以我们可以通过仅使用图像 来消除这种依赖关系 现在 当我再更改与图像无关的 狗的属性时 变更日志将不会出现
这个方法会严格限定视图的依赖项 如果你使用这种技术 请不要忘记 移除对 printChanges 的调用 让我们更新父视图以匹配 这是父级狗视图的代码 我需要更新 ScalableDogImage 的 初始化定式以获取图像 就像这样 通过将 ScalableDogImage 提取出来 我将依赖项 减少到最低 我也可以对 header 进行相同的操作 将其提取到自己的视图中 这样做有众多好处 现在这段代码更加易于阅读 而且 DogHeader 的依赖项 在其使用页面上十分明显 这种技术在较小的视图中非常好用 但在面对 非常大的 struct 时要小心 并不是每个依赖项都要被这样限定 开发者应根据实际情况进行判断
当 App 中的数据发生变化时 更新越少意味着性能越好 正如我们刚刚探讨的 方法之一是减少依赖项 你可以试着将视图的值 减少到只有实际依赖的数据 另一个方法是通过提取视图 来减少依赖项 最后 新的 Observable 协议 可以自动将依赖项减少为 仅被读取的内容 来帮助限定依赖项 请查看“探索 SwiftUI 中的 Observation”讲座以获取更多信息 以上就是关于 如何检查依赖项的快速介绍 让我们继续来讨论 如何让更新变得更加快速 在下一部分 我们将讨论 如何降低每个 SwiftUI 更新的成本 SwiftUI 更新缓慢可能会 对你的 App 产生许多负面影响 包括响应性降低 比如出现挂起和卡顿 挂起是指对用户交互的延迟响应 例如视图需要花很长时间 才能初始化 WWDC2023 的“使用 Instruments 分析挂起”讲座详细介绍了 如何使用 Instruments 来分析挂起 包括如何确定挂起是否 由与 SwiftUI 相关的工作引起 卡顿是指用户可以感知的动画问题 如滚动过程中的停顿 或者动画中的跳帧现象 挂起和卡顿的根本原因 通常是相关的 尤其是在 SwiftUI 中 想了解更多有关卡顿的信息 包括系统渲染循环的工作原理 请查看“探索 UI 动画卡顿和渲染循环” Tech Talks 视频 SwiftUI 中的挂起和卡顿 往往源于更新速度过慢 而更新缓慢有许多常见原因 首先是耗费资源的动态属性实例化 例如分配和初始化一个状态对象 或初始化状态 另一个原因是主体内部的工作 请确保检查耗费资源的 字符串插值或数据过滤等操作 以及主体内部的其他工作 重要的是 主体本身消耗的资源 要尽可能低 这些都是相互关联的 例如 一个动态属性 可能是从视图的主体 计算得出 使得视图评估 消耗的资源变多 缓慢的识别 也经常发生在视图的主体中 让我们先来看看 fetch App 中的示例
示例中 我正在处理 App 的根视图 它带有一个对象 我用它来创建狗列表 我们可以通过 本页上高亮显示的代码看到 该对象先访问了 主体中的 model.dogs 然后开始缓慢地实例化 接着来到了初始化定式 最后才抓取了狗列表 正如代码注释所说 这可能需要很长时间 这些都是同步工作 解决这个问题的一种方法 是使用 task 修饰符 我们首先将抓取过程变为 async 我在这里只展示 添加 async 关键字 接下来 在 task 修饰符中 我们将通过等待关键字 异步获取狗列表 这样 即使数据开始进行 耗费资源的加载 App 也可以保持响应性 你可能没有意识到还有其他工作来源 正在影响你的 App 例如 字符串插值 通常会很耗费资源 所以你应确保 缓存任何经常使用的字符串 同样地 从资源包中查找值 也很耗费资源 当然 任何堆内存分配 例如类绑定类型 也都会增加资源消耗 接下来 我们看看 List 和 Table 除了提供简约的布局外 List 和 Table 还支持更丰富的功能 如增加了选择、轻扫操作、 重新排序支持等等 这些都是复杂的高级控件 理解身份概念 可以确保 它们在你的 App 中表现良好 在这部分 我们将探讨 List 和 Table 中的身份 并揭秘如何 让这些内置组件的更新性能最大化 在我们深入这个话题之前 我想先介绍一些改进 在 macOS Sonoma 和 iOS 17 中 SwiftUI 针对过滤 和滚动等情况进行了许多优化 开发者只需付出最少的努力 即可获得这些改进 并且在许多情况下 这些改进能显著提升 大型 List 和 Table 的 响应加载和更新时间 然而 有一些 构建 List 和 Table 的方法 可以带来更好的性能 List 和 Table 使用标识符 来了解数据发生的变化 为了保持一致性 List 和 Table 的 所有 ID 都会被迅速收集到一起 因此 能够快速生成 List 和 Table 内容的标识符 直接转化为更快的加载和更新时间 SwiftUI 可以通过身份 来管理视图生命周期 这对于层次结构的 增量更新至关重要 身份的改变就意味着视图的改变 这对动画和性能来说非常重要 想了解有关动画的更多信息 请查看“SwiftUI 动画基础”讲座 身份识别性能非常重要 因为标识符需要经常被收集 特别是在 List 和 Table 中 让我们先来了解一下 List 识别模型 我正在努力创建 App 中的狗列表 我从只有一行开始 这是该 List 的代码 只有一个 DogCell 下一步是使用 ForEach 来迭代所有的狗 这个示例很简单 但它与身份直接相关 而在 List 中添加 ForEach 是评估性能的重要时机 为了理解原因 我们接下来 看看 ForEach 的通用签名 这是 SwiftUI 中的 ForEach 签名 ForEach 将数据集合 映射到一系列视图 为每个视图产生显式身份 当开发者使用 List 时 它需要确定要显示行数 以及每行的标识符是什么 因此 它会提前访问数据集合 确定每个元素的 ID 然后调用内容闭包来生成每个视图 接着 行会被按需创建 List 使用身份和内容的 复合体来创建其行 按需创建的行 与可见区域相关 加上一些系统决定的 用于预取或访问的缓冲区 随着视图滚动 更多的视图会出现 下面是生成 此 ForEach 的代码片段 请注意 内容只有 DogCell 它本身是一个单个视图 因为它在内部使用了 HStack ForEach 对于确定 List 所使用的最终行 ID 至关重要 而 List 需要提前知道所有的 ID 但只有当内容解析为 恒定的行数时 它才能在 不访问全部内容的情况下 高效地执行此操作 举个例子 假设我们想重构这个 List 让其只显示喜欢捡球的狗狗 你可能会想添加一个 条件视图的过滤器 就像这样 这里 视图的数量会变化 不是 1 就是 0 但这样非常不好 因为这会导致 List 需要构建所有视图来检索行标识符 因为它不知道每个元素 会解析为多少个视图 如果使用 AnyView 情况也是如此 这里 视图的数量现在完全未知 所以我们面对和之前一样的问题: 所有行都需要被创建 如果我们将过滤器 移到数据集合本身呢? 现在 每个元素只有 一个恒定数量的视图 并且只有需要的视图 才会构建它们的行内容 但是要注意:这里的内联过滤器 会对集合进行线性操作 这样做在原型中可能有效 但是当集合扩大时 这个操作可能很快会非常耗费资源 导致更新变慢 因此最好是将其移出模型 现在我们有了两全其美的结果: 过滤器被缓存了 所以它不会 在每次构建 List 时都运行 而且每个元素的视图数量 也是恒定的 以下是一些让你的视图数量 保持恒定的技巧 请注意 这种视图计数方法只适用于 在 List 和 Table 中 使用 ForEach 因为这些组件 会提前收集它们的标识符 正如我刚刚提到的 请避免使用 AnyView 和不平衡的条件 在适当的情况下 你还可以 使用显性的 stack 但请注意 某些修饰符 如 listRowBackground 需要放在 stack 之后 而不是之内 最后 请尽量展平 嵌套的 ForEach 结构 然而 有时候嵌套的 ForEach 也会派上用场 比如已分组的 List 让我们来看示例 在这个例子中 我有一个按照每只狗 最喜欢的玩具分组的狗列表 我将使用 ForEach 来创建动态数量的分组 并通过嵌套 ForEach 在每个分组内部创建动态数量的行 这个 List 需要检索所有标识符 但因为我们在这里使用了分组 SwiftUI 可以理解这种结构 并确保 List 仍然可以快速渲染 动态分组是使用 嵌套 ForEach 的一个典型例子 需要考虑的基本公式是 List 中的 ForEach 产生的行数等于元素数 乘每个元素产生的视图数 你需要确保 每个元素的视图数量是恒定的 否则 SwiftUI 除了标识符之外 还必须构建所有视图 才能识别行 目前为止 我们已经讨论了 List 但这些规则通常也适用于 Table Table 使用 TableRow 而不是视图 而且 TableRow 始终解析为单行 让我们看一个 Table 示例 这是一个狗表格 里面有一个 ForEach 因为 TableRow 始终是单行 所以这里的总行数 就是狗集合中的元素数 这种结构很常见 所以 SwiftUI 在 iOS 17 和 macOS Sonoma 中有一个新功能 它会提供一个简化的 初始化定式 你只需编写 数据集合的 ForEach 它就会为你创建表格的行 虽然这个初始化定式是新的 但它可以回溯到 之前所有支持 Table 的 操作系统版本 它不仅让构造更简单 还会强制规定 ForEach 内容的行数为恒定 这有助于提高识别性能 然而 我想指出 这里有一个新的语义变化 如果你的代码像这样 它在最新的 OS 版本中 可能会有不同的表现 在这个例子中 我们对狗进行了 ForEach 同时创建了一个狗的行 然而这里狗的值并不匹配 这里的值是狗的最好的朋友 在 iOS 16 中 每一行都由它的值来标识 但在 iOS 17 中 为了改进性能 这一行为已发生改变 其原因是 现在我们不需要 通过查看 ForEach 来识别每个 TableRow 因此 本例中现在变成了 有每只狗的 ID 而不是 TableRow 的值 如果开发者需要回退部署 可以通过映射集合 或显示指定 一个 ID 键值路径 来获取旧的行为
需要考虑的基本公式是 List 中的 ForEach 产生的行数等于元素数 乘每个元素产生的视图数 在 Table 中 情况类似 但是要乘每个元素的 TableRow 数 我们已经介绍了一些让 List 和 Table 运行更快的技巧 即开发者应创建耗能较低的标识符 以及保证 ForEach 内容中 视图数量恒定 今天我们介绍了很多东西 我们从探索依赖图开始 了解了依赖关系以及如何优化它们 接着 我们了解了更新缓慢问题 以及该如何提升响应性 最后 我们探讨了 List 和 Table 中 身份的重要性 建立了正确的心智模型后 你可以在开发的初始阶段 轻松获取出色的性能 这样你就可以 更加注重 App 的细节 感谢你的观看 ♪ ♪
-
-
3:59 - DogView
struct DogView: View { @Environment(\.isPlayTime) private var isPlayTime var dog: Dog var body: some View { Text(dog.name) .font(nameFont) Text(dog.breed) .font(breedFont) .foregroundStyle(.secondary) ScalableDogImage(dog) DogDetailView(dog) LetsPlayButton() .disabled(dog.isTired) } } }
-
4:00 - ScalableDogImage
struct ScalableDogImage: View { @State private var scaleToFill = false var dog: Dog var body: some View { dog.image .resizable() .aspectRatio( contentMode: scaleToFill ? .fill : .fit) .frame(maxHeight: scaleToFill ? 500 : nil) .padding(.vertical, 16) .onTapGesture { withAnimation { scaleToFill.toggle() } } } }
-
4:01 - printChanges
expression Self._printChanges()
-
4:02 - ScalableDogImage + printChanges
struct ScalableDogImage: View { @State private var scaleToFill = false var dog: Dog var body: some View { let _ = Self._printChanges() dog.image .resizable() .aspectRatio( contentMode: scaleToFill ? .fill : .fit) .frame(maxHeight: scaleToFill ? 500 : nil) .padding(.vertical, 16) .onTapGesture { withAnimation { scaleToFill.toggle() } } } }
-
8:46 - ScaleableDogImage
struct ScalableDogImage: View { @State private var scaleToFill = false var dog: Dog var body: some View { dog.image .resizable() .aspectRatio( contentMode: scaleToFill ? .fill : .fit) .frame(maxHeight: scaleToFill ? 500 : nil) .padding(.vertical, 16) .onTapGesture { withAnimation { scaleToFill.toggle() } } } }
-
8:47 - Updated DogView
struct DogView: View { @Environment(\.isPlayTime) private var isPlayTime var dog: Dog var body: some View { Text(dog.name) .font(nameFont) Text(dog.breed) .font(breedFont) .foregroundStyle(.secondary) ScalableDogImage(dog) DogDetailView(dog) LetsPlayButton() .disabled(dog.isTired) } } }
-
8:48 - Final DogView
struct DogView: View { @Environment(\.isPlayTime) private var isPlayTime var dog: Dog var body: some View { DogHeader(name: dog.name, breed: dog.breed) ScalableDogImage(dog.image) DogDetailView(dog) LetsPlayButton() .disabled(dog.isTired) } } }
-
12:22 - DogRootView and FetchModel
struct DogRootView: View { @State private var model = FetchModel() var body: some View { DogList(model.dogs) } } @Observable class FetchModel { var dogs: [Dog] init() { fetchDogs() } func fetchDogs() { // Takes a long time } }
-
12:23 - Updated DogRootView and FetchModel
struct DogRootView: View { @State private var model = FetchModel() var body: some View { DogList(model.dogs) .task { await model.fetchDogs() } } } @Observable class FetchModel { var dogs: [Dog] init() {} func fetchDogs() async { // Takes a long time } }
-
15:12 - List
List { ForEach(dogs) { DogCell(dog: $0) } }
-
16:08 - List Again
List { ForEach(dogs) { DogCell(dog: $0) } }
-
17:35 - List Fixed
List { ForEach(tennisBallDogs) { dog in DogCell(dog) } }
-
18:25 - Sectioned List
// Sectioned example struct DogsByToy: View { var model: DogModel var body: some View { List { ForEach(model.dogToys) { toy in Section(toy.name) { ForEach(model.dogs(toy: toy)) { dog in DogCell(dog) } } } } } }
-
19:21 - DogTable
struct DogTable: View { var dogs: [Dog] var body: some View { Table(of: Dog.self) { // Columns } rows: { ForEach(dogs) { dog in TableRow(dog) } } } }
-
19:22 - DogTable Brief
struct DogTable: View { var dogs: [Dog] var body: some View { Table(of: Dog.self) { // Columns } rows: { ForEach(dogs) } } }
-
20:06 - DogTable Different IDs
struct DogTable: View { var dogs: [Dog] var body: some View { Table(of: Dog.self) { // Columns } rows: { ForEach(dogs) { dog in TableRow(dog.bestFriend) } } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。