View in English

  • 打开菜单 关闭菜单
  • Apple Developer
搜索
关闭搜索
  • Apple Developer
  • 新闻
  • 探索
  • 设计
  • 开发
  • 分发
  • 支持
  • 账户
在“”范围内搜索。

快捷链接

5 快捷链接

视频

打开菜单 关闭菜单
  • 专题
  • 相关主题
  • 所有视频
  • 关于

返回 WWDC25

  • 简介
  • 概要
  • 转写文稿
  • 代码
  • 通过 Instrument 优化 SwiftUI 性能

    探索全新的 SwiftUI Instrument。我们将介绍 SwiftUI 如何更新视图、App 数据的变化对这些更新会产生什么影响,以及新的 Instrument 如何帮助你以可视化方式了解相应的原因和效果。 为了充分从这个讲座中获益,建议你先熟悉一下如何用 SwiftUI 编写 App。

    章节

    • 0:00 - 简介与内容安排
    • 2:19 - 探索 SwiftUI 工具
    • 4:20 - 诊断并修复视图 body 的长时间更新问题
    • 19:54 - 理解 SwiftUI 更新的因果
    • 35:01 - 后续步骤

    资源

    • Analyzing the performance of your visionOS app
    • Improving app responsiveness
    • Measuring your app’s power use with Power Profiler
    • Performance and metrics
    • Understanding and improving SwiftUI performance
      • 高清视频
      • 标清视频

    相关视频

    WWDC25

    • 通过 Instruments 优化 CPU 性能

    WWDC23

    • 使用 Instruments 分析挂起
    • 探索 SwiftUI 动画
    • 解密 SwiftUI 性能

    WWDC22

    • 使用 SwiftUI 构建自定布局

    WWDC21

    • 揭开 SwiftUI 的神秘面纱

    Tech Talks

    • 探索 UI 动画阻碍与渲染循环
  • 搜索此视频…

    大家好!我是来自 Instruments 团队的 Jed 我是 Steven 来自 Apple Music 团队 优秀的 App 需要有出色的性能 App 中运行的任何一段代码 都可能拖慢 App 的运行速度 因此 我们有必要分析 App 找出哪些代码 可能造成 App 的性能瓶颈 然后解决相应问题 让 App 顺畅运行 在今天的讲座中 我们将重点介绍 如何判断 SwiftUI 代码中的瓶颈 并介绍如何让 SwiftUI 更高效地运行 怎么第一时间知道 App 遇到了性能问题? 其中一个表现就是 App 响应速度变慢 出现卡顿或挂起情况 具体表现为动画暂停或跳帧 或是滚动出现延迟 发现性能问题的最好的方式是 利用 Instruments 对 App 进行分析 今天 我们将重点介绍如何诊断 SwiftUI 代码的性能问题 首先我们来介绍一下 Instruments 26 中 新增的 SwiftUI 工具 接下来 我们会介绍一个存在 视图主体更新耗时较长问题的 App 说明为什么这些是常见的性能问题 并使用工具查找和修复问题 最后 我们将深入介绍 SwiftUI 更新的因果关系 我们将使用工具来发现不必要的更新 并向大家展示如何避免这样的更新 导致性能问题的根本原因可能有很多 今天我们将重点介绍 使用 SwiftUI 导致的问题

    如果 App 的问题 与 SwiftUI 代码无关 我们建议你观看讲座 “使用 Instruments 分析挂起” 和“通过 Instruments 优化 CPU 性能” 并基于这些信息了解问题所在

    我和 Steven 一起开发了一款 App Steven 能给大家介绍一下 咱们的这款 App 吗? 谢谢 Jed! 这款 App 叫做“Landmarks” 里面有世界各地 的一些知名地标 每个地标都会显示 离我当前位置的距离 这样我就可以畅想一下要去哪里 是去一个要坐飞机才能到的地方 还是去附近的景点! 到目前为止 这个 App 表现还不错 但在测试的过程中 我注意到它滚动起来 没有我们想象的那么流畅 我想知道背后的原因 Jed 你提到了新的 SwiftUI 工具 能给我们介绍一下吗? 好的! 在 Instruments 26 中 我们推出了一种新方法 可以识别 SwiftUI App 的性能问题 新一代 SwiftUI 工具

    更新后的 SwiftUI 模板 包含几种不同的工具 可以帮助我们评估 App 的性能 首先我们提供了新的 SwiftUI 工具 我稍后会详细介绍

    其次 我们引入了 Time Profiler 它会显示一些示例 代表一段时间内 App 在 CPU 上的表现 最后 我们还提供了 挂起和卡顿诊断工具 用于跟踪 App 的响应情况

    在调查 App 的潜在性能问题时 第一步就是要 查看 SwiftUI 工具顶部的信息

    SwiftUI 工具轨道的第一条通道称为 “Update Groups” 它会在 SwiftUI 工作时显示

    如果在这个通道为空期间 CPU 使用率达到峰值 那么就表示 问题不是出在 SwiftUI 上 我们可以通过 SwiftUI 工具轨道 的其他通道 轻松找到耗时较长的 SwiftUI 更新及发生的时间

    “Long View Body Updates” 会突出显示加载时间太长的 视图的“body”属性 “Long Representable Updates” 会显示可能花费过长时间的 视图和视图控制器可视化更新

    最后“Other Long Updates”会显示 SwiftUI 耗时过长的其他问题 这 3 个通道会概括显示 所有耗时过长 可能会导致 App 性能不佳的更新 更新以橙色和红色显示 取决于它们导致卡顿或挂起的可能性 这些更新是否真的会导致 App 挂起或卡顿 可能取决于设备状况 但为了调查更新时间过长的问题 我们通常先从红色的更新入手

    要开始使用 SwiftUI 工具 需要安装 Xcode 26 然后 将你要运行和分析 App 的设备 更新到最新的操作系统版本 新版本支持记录 SwiftUI 跟踪数据 我想我们可以开始分析 Landmarks App 了 - Steven 你来吧! - 谢谢 Jed!

    这个项目已在 Xcode 中打开了 要开始分析 我会按下 Command+I 键 Xcode 就会以 Release 模式编译 App 然后会自动启动 Instruments

    我会从模板选择器中 选择 SwiftUI 模板

    然后点按录制按钮 开始录制

    首先 我会滚动浏览地标列表 每个大陆都有一个水平展示区

    我会横向滚动到北美展示区的末尾 以加载更多地标视图

    然后点按停止录制

    录制停止后 Instruments 会从我的设备 拷贝分析数据 并进行进一步的分析处理 处理完成后 我就可以使用 SwiftUI 工具 来确定这款 App 是否存在 任何需要注意的 潜在性能问题 我将这个窗口最大化 以便轻松查看所有内容

    我会先查看 SwiftUI 工具轨道 最上面一层 用时较长的更新通道

    运行时间过长的视图主体 是导致 SwiftUI 性能问题 的常见原因 因此 我要先检查 “Long View Body Updates”通道

    这条通道上有一些 橙色和红色耗时过长的更新 我要从这些更新入手 点按展开 SwiftUI 轨道

    这里有 3 个子轨道: “View Body Updates” “Representable Updates”和“Other Updates” 每个子轨道都有耗时过长的更新 以橙色和红色突出显示 就像最顶层的通道一样 其余更新以灰色显示 我将选择 “View Body Updates”轨道

    在下面的详细信息面板中 分析会话期间的所有视图主体 都有一个分层摘要 当我在这一层级中展开 App 的进程时 我可以看到运行的所有 视图主体更新的模块列表

    我可以点按下拉菜单 并选择 “Long View Body Updates”摘要 筛选出耗时较长的更新

    从数字可以看出 有几个更新需要进行调查

    点按以展开 App 的模块 LandmarkListItemView 有几个耗时 较长的更新 我会先从这个视图入手

    将鼠标悬停在视图名称上 会显示一个箭头

    点按箭头 会显示一个上下文菜单 我会选择“Show Updates” 就会显示这个视图主体中 所有耗时较长的更新的顺序列表

    右键点按其中一个耗时较长的更新 然后点按“Set Inspection Range and Zoom”

    这会将跟踪数据中的选中范围 设置为这个视图主体更新的间隔 然后点按 Time Profiler 工具轨道

    在这里 我们可以看到 视图主体运行时 CPU 的占用情况

    Time Profiler 通过定期 对 CPU 运行情况进行采样 来采集数据 对于每个样本 它会检查 当前正在运行的函数 并将信息保存到性能分析会话中 下面的 Profile 详细信息面板 会显示跟踪期间记录的样本 的调用堆栈 在本例中 这些是在我的 视图主体运行时记录的样本

    我会按住 Option 键并点按 以展开主线程调用堆栈

    SwiftUI 的运行情况 由一个非常深的调用堆栈表示 我最关心的是 LandmarkListItemView 我会按下 Command+F 键 搜索调用堆栈 然后在搜索栏中输入名称

    这是我的视图主体 最左侧一列 Time Profiler 会显示 调用堆栈中每一帧花费的时间

    这一列显示视图主体 耗时最多的项目是 一个名为“distance”的计算属性 在“distance”内 耗时最长的 两个帧 是对两个不同格式器的调用

    这个是测量格式器 这个是数字格式器 让我们切换回 Xcode 看看代码的情况

    这是 LandmarkListItemView 是列表中每个地标的视图

    这是我在 Time Profiler 中看到的“distance”属性 这个属性用于将我到地标的距离 转换为格式化字符串 以在视图中显示

    这是数字格式器 Time Profiler 表明 创建这个项目的开销很高

    这是测量格式器 创建字符串的地方 也是视图主体中耗时较多的项目

    在视图主体中 我会读取“distance”属性 以便为标签构建文本 每次视图主体运行时 都会发生这种情况 由于视图主体在主线程上运行 我的 App 必须等待距离文本格式化 然后它才能继续更新 UI

    但这为什么重要呢? 运行视图主体的毫秒级时间 可能看起来并不长 但所花费的总时间会累积起来 尤其是当 SwiftUI 要在屏幕上更新大量视图时 Jed 我们该怎么理解 SwiftUI 运行视图主体 所花费的时间? 这是一个好问题 首先 我将介绍渲染循环 在 Apple 平台上的工作机制 App 在每一帧都会“醒来”处理任务 例如触摸或按下按键 然后 App 会更新 UI 这包括运行任何已更改的 SwiftUI 视图的“body”属性 所有这些操作都必须 在每一帧截止时间之前完成 然后 App 会将工作移交给系统 让系统 在下一帧截止之前渲染视图 渲染的输出最终在这个截止时间 之后出现在屏幕上 这里的一切都按预期工作 更新会在相应的 帧截止时间之前完成 让系统有足够的时间来渲染每一帧 并在屏幕上呈现 我们将这种情况与因视图主体更新 耗时过长导致卡顿的 App 进行比较

    和之前一样 我们首先处理事件 然后运行 UI 更新 但是在第一帧 其中一个 UI 更新花费的时间太长 这导致 UI 更新部分 运行超过帧截止时间 这意味着下一次更新 要到下一帧后才能开始 而这个帧还没有准备好在 截止日期前将任何内容提交给渲染器

    因此 前一帧仍会显示在屏幕上 直到系统完成下一帧的渲染 我们把在屏幕上显示的时间过长 并导致后面的帧 发生延迟的帧 叫做卡顿 卡顿会让动画看起来不够流畅 关于卡顿的更多信息 请查看文章“了解 App 中的卡顿” 本次技术讲座将 更深入地探讨渲染循环 以及如何解决不同类型的卡顿 Steven 这是不是有助于解释 为什么视图主体运行时很重要? 是的 真的很有帮助! 因此 视图主体更新 运行耗时过长的风险在于 可能导致 App 错过帧截止时间 从而导致卡顿 因此 我需要一种方法来计算 每个地标的距离字符串 并在显示视图之前缓存 而不是在主体运行时才计算 接下来 我们回到代码部分

    这里是“distance”属性 每当视图主体更新时它都会运行 为了不在视图主体运行时 执行这项操作 我要把它移到更核心的位置 也就是我管理位置更新的类

    LocationFinder 类负责 在位置发生变化时接收更新 我不会在视图主体中计算 格式化的距离字符串 而是会提前创建这些字符串 并在这里进行缓存 所以当我的视图需要显示字符串时 这些字符串已经计算好了

    我会先更新构造器 创建我之前在视图主体中 创建的格式器

    我添加了这个名为 “formatter”的属性 来存储我的测量格式器

    在构造器的顶部 我创建了之前在视图中 创建的数字格式器

    还有测量格式器 我将它存储在我添加的新属性中 由于格式不会改变 在需要更新距离字符串时 我就可以重复使用格式器 并避免每次视图主体运行时 都重新创建新格式器的开销 接下来 我需要一种方法 来缓存字符串 这样我的视图就可在需要时使用它们 我将添加一些代码来管理这些更新

    我有一个数组用来存储地标 我将用它来计算距离

    我还使用了一个字典 可以在计算后缓存距离字符串

    这个函数叫做 updateDistances 每当我的位置发生变化时 它都会重新计算字符串

    在这里 我会使用格式器 来创建距离文本

    并将文本存储在这里的缓存中

    稍后 我将从我的视图中 调用最后一个函数 以获取缓存的文本

    现在 我需要做最后一件事 当我的位置更新时 我需要更新字符串的缓存

    点按跳转栏下拉菜单

    跳转到 didUpdateLocations 函数 当我的位置发生变化时 CoreLocation 会调用这个函数

    我将在这里调用 我创建的 updateDistances 函数

    现在 我将切换回我的视图

    我会更新视图 以使用缓存的值

    这些更改应该可以修复 视图主体更新缓慢的问题

    现在我们来看一个实施这些修复后的 Instruments 跟踪数据 验证一下情况是否有所改善

    当我选择 “View Body Updates”轨道时 详细信息面板中的 “Long View Body Updates”摘要 显示 LandmarkListItemView 对应的 耗时较长的更新已经消失

    摘要中仍列出了两个 耗时较长的视图主体更新 但需要注意的是 这些更新 发生在跟踪开始时 那时候 App 正在准备 渲染第一帧 在 App 启动后立即进行更新 耗时较长的情况并不少见 因为系统要同时构建 App 的初始视图层级 但这不会导致卡顿 这里要注意 会导致滚动时卡顿的 LandmarkListItemView 长耗时更新 现在已经修复 并且已经从列表中消失了 这意味着我可以确信 我没有拖慢 SwiftUI 的速度 它可以将我的所有视图 都显示在屏幕上

    修复视图主体更新耗时较长问题 确实是提升 App 性能的关键 但是 我们还要考虑其他因素 过多不必要的视图主体更新 也会导致性能问题 我来解释一下其中的原因

    这是 Jed 之前展示的图表 但这一次 没有一次更新 比其他更新耗时更长 相反 这里有大量相对较快的更新 都必须在这一帧中完成

    这些过多的更新 都会导致应用错过 提交帧的截止时间 同样 下一次更新也会延迟一帧 而且由于内容没能及时传递给渲染器 会再次出现卡顿 因为前一帧会显示整整两帧的时间 我之所以提到不必要的视图更新 对性能的潜在影响 是因为我一直在 为我们的 App 开发一项新功能 我认为这是一项重要的功能 滚动浏览所有地标 会让我对探索新去处感到非常兴奋 但真的很难决定要去哪里 所以我想到了一个办法 来简化这个决策过程 我来演示一下 我为每个地标添加了 一个新的爱心按钮 轻点一下就可以添加和取消收藏

    我来展示一下代码

    在 LandmarkListItemView 中 我添加了这个叠加层 用于显示我的新爱心按钮

    按钮的操作会调用 我的模型数据类上的 toggleFavorite 函数 以收藏或取消收藏地标

    如果地标已收藏 标签图标会显示一个实心图标 如果没有收藏 则显示空心图标

    按住 Command 键同时点按 toggleFavorite 可跳转到这个函数

    这就是我添加收藏的方式

    这个模型会存储收藏的地标的数组 在添加收藏时 我将地标附加到数组中

    如果要取消收藏 就将地标从数组中移出

    这些就是我目前的代码 我相信我的功能还需要改进 但我可以尽早在 Instruments 中进行分析 在开发过程中也需要经常分析 那么 我们来看看我的 新功能是怎么运作的 按下 Command+I 键来构建 App 然后切换回 Instruments

    再次点按录制

    我会像之前一样向下滚动到北美列表

    接着向右滚动

    点按爱心图标 就可以收藏缪尔森林这个景点了 因为这里离我住的地方不远 而且我还没有去过这里! 现在我继续向上滚动 收藏一个比较远的景点 富士山怎么样? 这将是一次有趣的旅程! 现在我将停止录制

    我想确保在轻点我的新收藏按钮时 不会导致任何不必要的更新 在分析跟踪数据时 要考虑自己的目标 并寻找任何看起来不正常的迹象 是识别潜在问题的好方法

    点按以展开 SwiftUI Instrument 轨道 然后选择 “View Body Updates”子轨道

    由于我收藏了两个景点 缪尔森林和富士山 我预计这两个视图已经更新 向下滚动到底后 我轻点了跟踪后半部分的按钮 所以我要突出显示 跟踪数据的这一部分 只关注我感兴趣的部分

    现在我来看看下面的详细信息面板 我将展开层次结构 以查找视图的更新列表

    我很惊讶地看到 LandmarkListItemView 实际上更新了很多次 这是为什么呢? 在 UIKit App 中 调试视图更新时 我通常会在代码中放置一个断点 并检查回溯栈跟踪 以尝试找出视图更新的原因 但在我的 SwiftUI App 中 比如 Landmarks 这种做法并没有奏效 SwiftUI 调用堆栈似乎更难理解 Jed 为什么这种方法 不适用于 SwiftUI App 呢? 我来解释一下 Xcode 可帮助我们理解 命令式代码的因果关系 就像在 UIKit App 中 点按断点就可以显示回溯栈跟踪 UIKit 是一个命令式框架 因此 回溯栈跟踪通常 可用于调试因果关系 在这里 我可以看出 我的标签正在更新 因为我在 viewDidLoad 中 设置了一个 isOn 属性 从回溯栈跟踪中 一些系统帧的名称可以猜出 这似乎发生在 我的 App 启动第一个场景时 与执行相同功能的类似 SwiftUI App 进行比较时 我发现 SwiftUI 内部的内容 有几处递归更新 由名为 AttributeGraph 中的帧隔开 这些都没能说明为什么 我的视图有更新的必要

    SwiftUI 是声明式的 我们不能使用回溯栈跟踪来了解 视图更新的原因 那么 我们该如何理解 导致 SwiftUI 视图更新的原因呢? 首先 我们需要了解 SwiftUI 的工作原理

    下面我们来看一个小的示例视图 看看 SwiftUI 的数据模型 AttributeGraph 怎么定义视图之间的依赖关系 并避免重新运行视图 除非确实有必要 今天我没办法介绍所有细节 但这一部分应该 能让大家基本理解 App 是怎么更新的

    视图声明符合 View 协议 然后 实现一个 body 属性 通过返回另一个 View 值 来定义它们的外观和行为

    这里的 OnOffView 从它的 body 返回 Text 视图 并传入一个会根据 isOn 状态变量的值而 更改的标签

    首次将这个视图添加到 视图层次结构中时 SwiftUI 会从存储 视图结构体的父级视图 接收一个名为属性的对象 视图结构体会频繁地重新创建 但属性会保持它的标识 并在视图的整个生命周期内 保持当前状态 因此 随着父视图的更新 这个属性的值将更改 但它的标识不会变 视图要创建自己的属性 来存储它的状态并定义它的行为 它首先为 isOn 状态变量创建存储 以及一个跟踪 状态变量何时变化的属性 然后 视图会创建一个 新属性来运行它的主体 这个主体取决于这两者 当 App 要求 view body 属性 生成一个新值时 它会读取父级视图传递的 视图的当前值 接下来 属性会使用状态变量的 当前值 更新这个视图结构体的副本 然后它会访问 视图临时副本上“body”的计算属性 并将返回的值保存为 这个属性的更新值 然后 由于视图的 主体返回的是 Text 视图 SwiftUI 设置了显示文本所需的属性

    Text 视图会创建一个 依赖于环境的属性 来访问当前的默认样式 如前景色和字体 以确定任何呈现的文本 应该是什么样子 这个属性会为视图主体 添加一个依赖项 用来访问我们从返回的 Text 结构体 呈现的字符串 最后 Text 会创建另一个属性 它根据样式化的文本 构建要渲染的内容的描述

    现在 我们看看 更改状态变量时会发生什么 当你这样做时 SwiftUI 不会 立即更新视图 而是会创建一个新事务 这个事务表示对 SwiftUI 视图 层次结构的更改 需要在下一帧之前完成

    这个事务会将状态变量 的 signal 属性 标记为“Outdated” 然后 当 SwiftUI 准备好 为下一帧进行更新时 它会运行事务并应用计划的更新 这时候属性已标记为“Outdated”

    SwiftUI 会沿着 依赖于这些“Outdated”属性 的属性链 通过设置标志将 每个属性标记为“Outdated” 设置标记的速度非常快 尚未出现不必要的操作 运行任何其他事务之后 SwiftUI 现在需要确定 要怎么在屏幕上呈现这一帧 但它无法访问这些信息 因为它已被标记为“Outdated”

    因此 SwiftUI 必须更新 这些信息的所有依赖项 来决定要呈现什么

    首先是那些没有 “Outdated”依赖项的参数 比如 State 信号 现在 视图主体属性已做好更新准备 它再次运行 生成一个全新的 Text 结构体值和一个更新的字符串 这将传递给现有的应用样式属性 并且更新会一直继续直到确认 呈现内容所需的所有属性都已经更新 现在 SwiftUI 能够回答 它所要解决的问题 要在屏幕上呈现什么?

    如果我问“为什么我的视图主体 会运行?”真正的问题实际上是 “是什么将我的视图正文 标记为‘Outdated’?” 我们通常可以控制何时让依赖项 (比如其他视图) 将视图内容标记为“Outdated” 尤其是当这些视图是自己创建的时候 但为了显示你的视图 SwiftUI 还会执行其他操作 这项工作是必要的 通常都不可避免的 了解何时发生可能具有重要意义 向开发者提供 视图更新的原因和结果 是新 SwiftUI 工具的一项重要功能 因果图记录了所有这些因果关系 并会显示一张这样的图

    我们从正在调查的 视图主体更新开始 更新显示为带有图标的节点 表示这是视图主体更新 还有一个标题 说明它对应于哪种视图类型

    有一个指向它的箭头 箭头的起点是表示状态更改的节点 箭头标记为“Update” 因为是状态更改导致了视图更新 还有一个 标为“Creation”的边 会说明是什么让视图 首次出现在视图层次结构中

    状态更改节点的标题会指示 状态变量的名称 以及它所附加到的视图类型 选择状态更改时 我们可以看到 值更新位置的回溯栈跟踪

    继续向因果图的左侧看 我们可以看出状态变化是因为手势 例如点按按钮

    Steven Landmarks App 的因果图上都有什么 我们来看看因果图 弄清楚为什么会发生所有这些 额外的视图主体更新

    这是因果图视图 LandmarkListItemView.body 的节点已经被选中了 图中的蓝色节点代表 我自己的一部分代码 或者我在与 App 交互时执行的操作 图上从左到右显示了因果链

    “Gesture”节点表示 我点按收藏按钮的操作

    这导致了收藏地标的数组需要更新

    从而让 LandmarkListItemView 的 主体更新了很多次 更新次数远超出了我的预期

    就像轻点一个收藏按钮一样 可能会导致屏幕上的 多个项目视图都发生了更新 而不是只更新我轻点的那个 我们回到代码中 弄清楚这里发生了什么

    现在 我将切换回 LandmarkListItemView

    我通过调用 modelData.isFavorite 并传递地标来 确认地标是否被标记为收藏 ModelData 是我的顶层模型对象 它使用 @Observable 宏 让 SwiftUI 在视图属性 发生变化时更新我的视图 按住 Command 键同时点按 isFavorite 可跳转到这个函数

    现在我可以访问 favoritesCollection.landmarks 数组 确认这个地标是否已经收藏 这会导致 @Observable 在每个项目视图 和整个收藏数组之间建立依赖关系 所以每当我向数组中添加收藏时 每个项目视图的主体都会运行 因为数组已经改变 现在 我来介绍一下它的工作原理

    这是我的一些 LandmarkListItemView 这是我的 ModelData 类 其中包含 favoritesCollection 它会持续跟踪 我收藏的地标 目前 我只收藏了 2 号地标 � ModelData 类有一个 isFavorite 函数 每个 LandmarkListItemView 都会调用这个函数 来确定是否应高亮显示图标

    isFavorite 函数会检查集合 确定是否包含地标 并且每个视图都呈现了自己的按钮 由于每个视图都访问了 收藏地标的数组 尽管不是直接访问的 @Observable 宏为整个收藏地标的 数组中的每个视图都创建了依赖项

    那么当我想在一个 其他视图中轻点收藏按钮 添加新的收藏时会发生什么? 视图调用 toggleFavorite 这会为我的收藏添加一个新的地标 因为我所有的 LandmarkListItemView favoritesCollection 上 都有一个依赖项 所有视图都标记为“Outdated” 并且它们的主体会再次运行

    但这不是理想情况 因为我只想更改 3 号视图 我真正需要的是让视图的 数据依赖关系更加精细 以便当 App 的数据发生变化时 仅更新必要的视图主体

    我们来重新考虑一下 我知道我的每一个视图 都有一个地标 每个地标都有一个收藏状态 “已收藏”或“未收藏”

    为了跟踪这个状态 我要为视图创建一个 可观测的视图模型 模型有一个 isFavorite 属性 用来跟踪收藏状态 并且每个视图都有自己的视图模型

    现在我可以将视图模型 存储在 ModelData 类中 每个视图都可以检索自己的模型 并根据需要打开和关闭收藏 因此 与其让每个视图都 依赖于整个收藏地标的数组 每个视图仅直接 依赖于它自己地标的视图模型 我再添加一个收藏地标! 轻点这个按钮会调用 toggleFavorite 这将更新 1 号视图的视图模型 由于 1 号视图只依赖于 它自己的视图模型 所以只有这个视图的主体会再次运行 我们看看在 Landmark App 中 做出这些更改的效果如何

    这是我实现新视图模型 改进后记录的跟踪记录 再次点按 “View Body Updates”子轨道 我将选择时间线中 与之前相同的部分

    在详细信息面板中 我会展开进程

    和 Landmarks 模块

    现在 只有两个更新 因为我更改了两个收藏地标 这一点没问题 我们仔细看一下这个图 我会将鼠标悬停在视图名称上 然后点按箭头 选择 “Show Cause & Effect Graph”

    这张图就又出现了

    现在从 @Observable 节点 到视图主体的箭头 仅显示两个更新 每个按钮一个更新 通过替换每个项目视图 对整个收藏地标数组的依赖 借助一个紧密耦合的视图模型 我避免了大量不必要的 视图主体更新 这样能让我的 App 更顺畅地运行 这个示例中的图比较小 因为让我的视图主体 更新的原因非常有限 如果原因变多 这个图可能会变得很大 发生这种情况可能是 视图从环境中读取数据 Jed 能给我们举一个例子吗? 好的! 首先我来谈谈环境是如何运作的 环境中的值存储在 EnvironmentValues 结构体中 这是一种类似于字典的值类型 这些视图中的每一个都依赖于 整个 EnvironmentValues 结构体 因为每个视图都会使用 environment 属性封装器 访问相应环境 当环境中的任何值更新时 每个依赖于环境的视图都会 收到通知 告知主体可能需要运行 然后 每个视图都会检查 它正在读取的值是否已更改 如果值发生更改 则视图主体需要再次运行 如果没有变化 SwiftUI 可以跳过运行视图主体 因为视图已经是最新的 我们来看看这些更新 在因果图中是什么样的

    图中有两种主要类型的节点 表示对环境的更新 外部环境更新包括 App 级内容 例如从 SwiftUI 外部更新的 配色方案 EnvironmentWriter 更新表示 对环境中值的更改 这些更改发生在 SwiftUI 内部 使用 dot-environment 修饰符 在 App 中进行的更新 也属于这一类 比如配色方案环境值 因为设备切换到深色模式而更新 这些视图的因果图 会是什么样子的? 这个图将显示 View1 的“外部环境”节点 由于配色方案是系统级环境更新 这张图上还将显示一个节点 指示 View1 的主体已运行 由于 View2 也会读取环境 它在图中的更新原因 也是外部环境更新 但 View2 不会读取配色方案值 所以主体不会运行 在图中 主体没有运行的视图 由暗色的图标表示 在本例中 这两个外部环境节点 表示相同的更新 如果将鼠标悬停在 同一更新的任一节点上或点按 两个环境节点都会高亮显示 以便于识别 如图中所示 这两个视图都发生了更新 因为即使视图的主体 不需要因为环境更新而运行 仍然会产生开销 因为要检查视图中关注点的值更新 如果 App 要从环境中读取很多视图 花费的时间可能很快就会增加 因为这个原因 一定要避免 存储频繁更新的值 例如环境中的几何值或计时器 这就是因果图 这样能很直观呈现 数据在 App 中的流动方式 能确保不会发生不必要的视图更新 在这次的讲座中 我们介绍了一些最佳实践 有助于让 SwiftUI App 实现出色的性能 一定要保证视图主体的速度 这样 SwiftUI 才能有足够的时间 让你的 UI 立即显示在屏幕上 不必要的视图主体更新会越积累越多 应将数据流设计为 仅在必要时更新视图 并注意频繁更改的依赖项 最后 记住在开发过程中 要尽早使用 Instruments 并经常分析 App 的性能 我们今天介绍了很多内容 最重要的一点是 确保视图主体快速更新 并且仅在需要时更新 这样才能实现出色的 SwiftUI 性能 在整个过程中使用 SwiftUI 工具 验证 App 的性能

    在今天的讲座中 我们介绍了 如何使用 SwiftUI 工具分析 App 这个工具还有很多 值得深入探索的功能 如果要了解这款工具的其他功能 可以看看视频说明中链接的文档 我们还提供了更多 视频和参考资料的链接 可以帮助大家分析和改进 App 性能 感谢大家观看本次讲座! 希望大家能借助新的 SwiftUI 工具 让 App 达到最佳性能

    • 8:47 - LandmarkListItemView

      import SwiftUI
      import CoreLocation
      
      /// A view that shows a single landmark in a list.
      struct LandmarkListItemView: View {
          @Environment(ModelData.self) private var modelData
      
          let landmark: Landmark
      
          var body: some View {
              Image(landmark.thumbnailImageName)
                  .resizable()
                  .aspectRatio(contentMode: .fill)
                  .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                  .overlay { ... }
                  .clipped()
                  .cornerRadius(Constants.cornerRadius)
                  .overlay(alignment: .bottom) {
                      VStack(spacing: 6) {
                          Text(landmark.name)
                              .font(.title3).fontWeight(.semibold)
                              .multilineTextAlignment(.center)
                              .foregroundColor(.white)
      
                          if let distance {
                              Text(distance)
                                  .font(.callout)
                                  .foregroundStyle(.white.opacity(0.9))
                                  .padding(.bottom)
                          }
                      }
                  }
                  .contextMenu { ... }
          }
      
          private var distance: String? {
              guard let currentLocation = modelData.locationFinder.currentLocation else { return nil }
              let distance = currentLocation.distance(from: landmark.clLocation)
      
              let numberFormatter = NumberFormatter()
              numberFormatter.numberStyle = .decimal
              numberFormatter.maximumFractionDigits = 0
      
              let formatter = MeasurementFormatter()
              formatter.locale = Locale.current
              formatter.unitStyle = .medium
              formatter.unitOptions = .naturalScale
              formatter.numberFormatter = numberFormatter
              return formatter.string(from: Measurement(value: distance, unit: UnitLength.meters))
          }
      }
    • 12:13 - LocationFinder Class with Cached Distance Strings

      import CoreLocation
      
      /// A class the app uses to find the current location.
      @Observable
      class LocationFinder: NSObject {
          var currentLocation: CLLocation?
          private let currentLocationManager: CLLocationManager = CLLocationManager()
      
          private let formatter: MeasurementFormatter
      
          override init() {
              // Format the numeric distance
              let numberFormatter = NumberFormatter()
              numberFormatter.numberStyle = .decimal
              numberFormatter.maximumFractionDigits = 0
      
              // Format the measurement based on the current locale
              let formatter = MeasurementFormatter()
              formatter.locale = Locale.current
              formatter.unitStyle = .medium
              formatter.unitOptions = .naturalScale
              formatter.numberFormatter = numberFormatter
              self.formatter = formatter
      
              super.init()
              
              currentLocationManager.desiredAccuracy = kCLLocationAccuracyKilometer
              currentLocationManager.delegate = self
          }
      
          // MARK: - Landmark Distance
      
          var landmarks: [Landmark] = [] {
              didSet {
                  updateDistances()
              }
          }
      
          private var distanceCache: [Landmark.ID: String] = [:]
      
          private func updateDistances() {
              guard let currentLocation else { return }
      
              // Populate the cache with each formatted distance string
              self.distanceCache = landmarks.reduce(into: [:]) { result, landmark in
                  let distance = self.formatter.string(
                      from: Measurement(
                          value: currentLocation.distance(from: landmark.clLocation),
                          unit: UnitLength.meters
                      )
                  )
                  result[landmark.id] = distance
              }
          }
      
          // Call this function from the view to access the cached value
          func distance(from landmark: Landmark) -> String? {
              distanceCache[landmark.id]
          }
      }
      
      extension LocationFinder: CLLocationManagerDelegate {
          func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
              switch currentLocationManager.authorizationStatus {
              case .authorizedWhenInUse, .authorizedAlways:
                  currentLocationManager.requestLocation()
              case .notDetermined:
                  currentLocationManager.requestWhenInUseAuthorization()
              default:
                  currentLocationManager.stopUpdatingLocation()
              }
          }
          
          func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
              print("Found a location.")
              currentLocation = locations.last
              // Update the distance strings when the location changes
              updateDistances() 
          }
          
          func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
              print("Received an error while trying to find a location: \(error.localizedDescription).")
              currentLocationManager.stopUpdatingLocation()
          }
      }
    • 16:51 - LandmarkListItemView with Favorite Button

      import SwiftUI
      import CoreLocation
      
      /// A view that shows a single landmark in a list.
      struct LandmarkListItemView: View {
          @Environment(ModelData.self) private var modelData
      
          let landmark: Landmark
      
          var body: some View {
              Image(landmark.thumbnailImageName)
                  .resizable()
                  .aspectRatio(contentMode: .fill)
                  .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                  .overlay { ... }
                  .clipped()
                  .cornerRadius(Constants.cornerRadius)
                  .overlay(alignment: .bottom) { ... }
                  .contextMenu { ... }
                  .overlay(alignment: .topTrailing) {
                      let isFavorite = modelData.isFavorite(landmark)
                      Button {
                          modelData.toggleFavorite(landmark)
                      } label: {
                          Label {
                              Text(isFavorite ? "Remove Favorite" : "Add Favorite")
                          } icon: {
                              Image(systemName: "heart")
                                  .symbolVariant(isFavorite ? .fill : .none)
                                  .contentTransition(.symbolEffect)
                                  .font(.title)
                                  .foregroundStyle(.background)
                                  .shadow(color: .primary.opacity(0.25), radius: 2, x: 0, y: 0)
                          }
                      }
                      .labelStyle(.iconOnly)
                      .padding()
                  }
          }
      }
    • 17:20 - ModelData Class

      /// A structure that defines a collection of landmarks.
      @Observable
      class LandmarkCollection: Identifiable {
          // ...
          var landmarks: [Landmark] = []
          // ...
      }
      
      /// A class the app uses to store and manage model data.
      @Observable @MainActor
      class ModelData {
          // ...
          var favoritesCollection: LandmarkCollection!
          // ...
      
          func isFavorite(_ landmark: Landmark) -> Bool {
              var isFavorite: Bool = false
              
              if favoritesCollection.landmarks.firstIndex(of: landmark) != nil {
                  isFavorite = true
              }
              
              return isFavorite
          }
      
          func toggleFavorite(_ landmark: Landmark) {
              if isFavorite(landmark) {
                  removeFavorite(landmark)
              } else {
                  addFavorite(landmark)
              }
          }
      
          func addFavorite(_ landmark: Landmark) {
              favoritesCollection.landmarks.append(landmark)
          }
      
          func removeFavorite(_ landmark: Landmark) {
              if let landmarkIndex = favoritesCollection.landmarks.firstIndex(of: landmark) {
                  favoritesCollection.landmarks.remove(at: landmarkIndex)
              }
          }
          // ...
      }
    • 20:50 - OnOffView

      struct OnOffView: View {
          @State private var isOn = true
          var body: some View {
              Text(isOn ? "On" : "Off")
          }
      }
    • 29:21 - Favorites View Model Class

      @Observable class ViewModel {
          var isFavorite: Bool
          
          init(isFavorite: Bool = false) {
              self.isFavorite = isFavorite
          }
      }
    • 29:21 - ModelData Class with New ViewModel

      @Observable @MainActor
      class ModelData {
          // ...
          var favoritesCollection: LandmarkCollection!
          // ...
      
          @Observable class ViewModel {
              var isFavorite: Bool
              init(isFavorite: Bool = false) {
                  self.isFavorite = isFavorite
              }
          }
      
          // Don't observe this property because we only need to react to changes
          // to each view model individually, rather than the whole dictionary
          @ObservationIgnored private var viewModels: [Landmark.ID: ViewModel] = [:]
      
          private func viewModel(for landmark: Landmark) -> ViewModel {
              // Create a new view model for a landmark on first access
              if viewModels[landmark.id] == nil {
                  viewModels[landmark.id] = ViewModel()
              }
              return viewModels[landmark.id]!
          }
      
          func isFavorite(_ landmark: Landmark) -> Bool {
              // When a SwiftUI view, such as LandmarkListItemView, calls
              // `isFavorite` from its body, accessing `isFavorite` on the 
              // view model here establishes a direct dependency between
              // the view and the view model
              viewModel(for: landmark).isFavorite
          }
      
          func toggleFavorite(_ landmark: Landmark) {
              if isFavorite(landmark) {
                  removeFavorite(landmark)
              } else {
                  addFavorite(landmark)
              }
          }
      
          func addFavorite(_ landmark: Landmark) {
              favoritesCollection.landmarks.append(landmark)
              viewModel(for: landmark).isFavorite = true
          }
      
          func removeFavorite(_ landmark: Landmark) {
              if let landmarkIndex = favoritesCollection.landmarks.firstIndex(of: landmark) {
                  favoritesCollection.landmarks.remove(at: landmarkIndex)
              }
              viewModel(for: landmark).isFavorite = false
          }
          // ...
      }
    • 31:34 - Cause and effect: EnvironmentValues

      struct View1: View {
          @Environment(\.colorScheme)
          private var colorScheme
      
          var body: some View {
              Text(colorScheme == .dark
                      ? "Dark Mode"
                      : "Light Mode")
          }
      }
      
      struct View2: View {
          @Environment(\.counter) private var counter
      
          var body: some View {
              Text("\(counter)")
          }
      }
    • 0:00 - 简介与内容安排
    • 了解如何借助 Instruments 26 中全新的 SwiftUI 工具和模板,优化 SwiftUI App 的性能。你可以使用 Instruments 对 App 进行性能分析,识别可能造成卡顿、挂起、动画与过渡中断或滚动延迟的问题瓶颈,例如视图主体更新耗时过长或不必要的 SwiftUI 更新。 示例 App“Landmarks”展示了全球各地的地标以及它们与用户当前位置的距离。通过这个示例,了解如何使用全新的 SwiftUI 工具来诊断并解决 SwiftUI 代码中的性能问题,从而提升 App 的滚动流畅度。

    • 2:19 - 探索 SwiftUI 工具
    • Instruments 26 推出了一个新的 SwiftUI 工具和模板,用于对 SwiftUI App 进行性能分析。与 Time Profiler、Hangs 和 Hitches 工具类似,它可以帮助识别性能问题。“Update Groups”轨道展示了 SwiftUI 的处理工作。其余三条轨道分别突出显示了“Long View Body Updates”、“Long Representable Updates”和“Other Updates”,并根据其可能引发卡顿或挂起的可能性,以橙色或红色标记。要使用全新的 SwiftUI 工具,请安装 Xcode 26,并将设备的操作系统更新至最新版本。

    • 4:20 - 诊断并修复视图 body 的长时间更新问题
    • 这个示例使用 Xcode 26 和 Instruments 26 对以 SwiftUI 编写的“Landmarks”App 进行性能分析。首先,启动 Instruments,选择“SwiftUI”模板以记录 App 的性能表现。 然后,在 iPhone 上与 App 交互,滚动地标列表以载入更多视图。录制结束后,Instruments 会处理数据,接着你就可以分析 SwiftUI 轨道了。重点查看“Long View Body Updates”轨道,你可以在其中识别出导致性能问题的具体视图,例如“LandmarkListItemView”。 通过展开 SwiftUI 轨道并使用 Time Profiler 工具,你可以更深入地分析视图主体更新期间的 CPU 使用情况。你可能会发现某些计算属性,尤其是用于转换和显示距离数据的格式化程序,占用了过多的处理时间。 在 SwiftUI 中,优化视图主体运行时至关重要。因为视图主体在主线程上执行,任何延迟都可能导致 App 错过帧截止时间,从而引发卡顿。卡顿会导致动画不流畅,影响整体用户体验。 为解决示例项目中的这些性能问题,你可以预先计算并缓存距离字符串,而不是在视图主体更新时进行计算,从而提升 App 的流畅度与响应速度。 在 Xcode 中,“LocationFinder”类负责管理位置更新,并进行了相关的优化处理。此前,系统在“LandmarkListItemView”的视图主体中计算格式化的距离字符串,导致更新效率低下。为了解决这个问题,代码将这个逻辑移至“LocationFinder”类中进行处理。在这里,系统在构造器中创建并存储格式化程序,以便后续重复使用,从而避免重复创建。 字典会在计算后缓存距离字符串。每当位置发生变化时,“updateDistances”函数负责重新计算这些距离字符串。这个函数使用预先创建的格式化程序生成距离字符串,并将它存入缓存中。 当设备位置发生变化时,CoreLocation 框架会在它的“CLLocationManagerDelegate”对象上调用“locationManager(_:didUpdateLocations:)”方法。通过在这个方法中调用“updateDistances”,可以确保缓存始终保持最新。视图随后从缓存中读取距离字符串,从而避免在视图主体更新过程中进行重复计算。 接下来,你可以添加一项新功能:心形按钮,用于收藏地标。当用户轻点这个按钮时,会调用“toggleFavorite”函数,更新模型数据类,将这个地标添加或移出收藏列表。视图会随之更新,显示实心或空心的心形图标以反映状态变化。 在 Instruments 中分析 App 新增的收藏功能时,你可能会发现“LandmarkListItemView”的更新频率高于预期。这一意外行为促使我们排查视图更新逻辑,并突显在 SwiftUI App 中调试视图更新所面临的挑战。与 UIKit App 相比,SwiftUI 属于声明式框架,传统基于断点的调试方式在这里并不如命令式架构那样直观。

    • 19:54 - 理解 SwiftUI 更新的因果
    • 在 Xcode 中,调试命令式代码 (如 UIKit App) 相对直观,可通过回溯轻松定位问题。然而,由于 SwiftUI 的声明式特性,这种调试方式在其中的效果并不理想。SwiftUI 使用名为“AttributeGraph”的数据模型来管理视图之间的依赖关系,从而优化视图更新。 在声明 SwiftUI 视图时,视图遵循“View”协议,并借由“body”属性定义它的外观和行为。“body”属性会返回另一个符合“View”协议的值,SwiftUI 会在内部通过属性来管理视图的状态和更新。 对状态变量的更改会触发事务,并将相关属性标记为过期。SwiftUI 会在下一个渲染帧中高效更新视图层次结构,沿着依赖链遍历,仅刷新必要的部分。 要了解 SwiftUI 视图为何更新,可以使用全新的 SwiftUI 工具中的因果关系图。这个图形以可视化方式呈现各类更新之间的关系,清晰展示从用户交互 (例如手势) 到状态变更,最终触发视图主体更新的因果链条。通过查看这个图形,你可以识别出低效之处 (例如,不必要的更新等),并相应地优化它们的代码。 在“Landmarks”App 中,“ModelData”类包含一个名为“favoritesCollection”的属性,用于将收藏的地标存储在数组中。最初,每个“LandmarkListItemView”都通过访问完整的“favoritesCollection”数组来判断地标是否已经被收藏,从而在每个列表项视图和整个数组之间创建依赖关系。这造成了性能低下,因为每当新增一个收藏项时,所有列表项视图的主体都会重新运行。 为了解决这一问题,我们重新设计了实现方式。为每个地标创建了一个符合“Observable”协议的数据模型,直接存储它的收藏状态。现在,每个“LandmarkListItemView”都拥有自己的数据模型,从而无需再依赖整个收藏地标数组。 通过实现这一更改,系统在用户切换收藏状态时只更新所需的视图主体。这项优化显著提升了性能,从因果关系图中视图主体更新次数的减少即可清楚看出成效。 这个图形还展示了环境更新 (例如颜色方案的切换) 如何影响视图。即使某些视图在环境更新时无需重新运行主体,系统仍需为这些变更执行检查操作,因此应避免将频繁变化的值存储在环境中,以减少相关成本。

    • 35:01 - 后续步骤
    • 关于 Instruments 26 中全新的 SwiftUI 工具,开发者文档中提供了更多功能说明、教学视频及相关资源,帮助你深入了解 App 性能分析与优化改进。

Developer Footer

  • 视频
  • WWDC25
  • 通过 Instrument 优化 SwiftUI 性能
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • App 扩展
    • App Store
    • 音频与视频 (英文)
    • 增强现实
    • 设计
    • 分发
    • 教育
    • 字体 (英文)
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 机器学习与 AI
    • 开源资源 (英文)
    • 安全性
    • Safari 浏览器与网页 (英文)
    打开菜单 关闭菜单
    • 完整文档 (英文)
    • 部分主题文档 (简体中文)
    • 教程
    • 下载 (英文)
    • 论坛 (英文)
    • 视频
    打开菜单 关闭菜单
    • 支持文档
    • 联系我们
    • 错误报告
    • 系统状态 (英文)
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件 (英文)
    • 反馈助理
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program (英文)
    • News Partner Program (英文)
    • Video Partner Program (英文)
    • 安全赏金计划 (英文)
    • Security Research Device Program (英文)
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖 (英文)
    • Apple 设计大奖
    • Apple Developer Academies (英文)
    • WWDC
    获取 Apple Developer App。
    版权所有 © 2025 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则