SwiftUI View 的 Identity 与 Lifetime
前阵子遇到一个很奇怪的 BUG:用 NavigationStack push 一个 View,CPU 使用率竟然冲上 100 %!当 Xcode 运行一段 SwiftUI 代码时,我们应该关注哪些方面?特此回顾 WWDC 21 Demystify SwiftUI。
当 Xcode 运行一段 SwiftUI 代码时,我们应该关注哪些方面?
- Identity—是 SwiftUI 用来区分视图元素的“身份证”
- Lifetime—是 SwiftUI 用来管理视图与其相关状态的生命周期
- Dependency—是 SwiftUI 用来决定视图为何以及何时需要更新
Identity
SwiftUI 中 Identity 分为两种:Explicit Identity 和 Structural Identity。

当遇到两个外表很相似的双胞胎,我们该如何区分?对,可以通过姓名来区分!这就是 Explicit Identity。
我查了点资料,我们仅知道这对著名的双胞胎姐妹是格雷迪双胞胎姐妹,并不知道各自的姓名。那我问你:你觉得哪个更恐怖一点?我是觉得左边那位更恐怖一些。
通过她俩的相对位置来区分:一位是站在左边,另一位是站在右边。这就是 Structural Identity。
接下来,我们来看一个更实际的例子:

左边的代码在 SwiftUI “眼里”当条件为真时显示 AdoptionDirectory 视图,条件为假时显示 DogList 视图。只要条件恒定,SwiftUI 能知道哪个是“真分支”哪个是“假分支”。
View body 中的 if 语句在 SwiftUI 里会被编译成 _ConditionalContent<TrueView, FalseView> 这样的泛型类型,TrueView 和 FalseView 是两个分支的具体类型。这个转换由 ViewBuilder 完成。
View 协议的 body 会隐式包裹在 ViewBuilder 中,ViewBuilder 会把 body 里的条件逻辑拼成一个统一的泛型视图类型。
Structural Identity 是 Swift 通过视图元素在视图层级中的类型与位置来识别视图身份的方法。
PawView 示例
在这个 代码示例 里,我们可以通过点击事件来更新 dog.isGood 状态,从而触发 pawView 的刷新。
1 |
|

问题一:为什么是绿色爪印消失后再出现红色爪印呢?
因为 SwiftUI 是通过 Structural Identity 来区分绿色爪印和红色爪印,当 dog.isGood 变化时,SwiftUI 认为它需要销毁绿色爪印视图和创建红色爪印视图。
不难得出:视图的 Identity 发生变化后,SwiftUI 会认为它需要销毁旧视图并创建新视图。
问题二:单个爪印直接从绿色过渡到红色应该会更丝滑,我要怎么做?
1 | var pawView: some View { |
我们可以把 pawView 里的 if 分支拿掉,单个 PawView 的 Structural Identity 就保持不变;dog.isGood 变化时,只是同一个视图的 tintColor 和位置在变,状态与动画都能连续。

Lifetime
我们给 PawView 加上 PawViewState 状态,来看看 Structural Identity 的变化对视图状态的影响
1 | class PawViewState { |
在我触发两次点击事件(Good dog → Bad dog, Bad dog → Good dog)后,Xcode console 输出如下:
1 | PawViewState(39BFC159-AE06-45A5-9083-B1C5BDF053C3 init at 2025-11-30 05:42:02 +0000 // Good dog 1 init |
从上面的例子中总结:
- 视图的 Identity 发生变化后,SwiftUI 会认为它需要销毁旧视图并创建新视图。也就是说,视图的生命周期是否延续取决于 Identity 是否保持不变。
- SwiftUI 里状态的生命周期和视图的生命周期是绑定在一起的。也就是说,一个视图被创建时状态才会被创建,视图被销毁时状态就随之消失。
因此,可得:
SwiftUI 把视图的“本质”归结为它持有的状态,并把这份状态和视图的身份绑定在一起。
身份不变,状态就被保留下来;身份一变,SwiftUI 就当成全新的视图,状态也重建。这样能清晰地区分:哪些状态应该延续(身份稳定的视图),哪些状态应该重置(身份变化的视图)
FavoritePetsView 示例
再看这个 例子,来辅助理解上面的总结。

1 | struct Pet: Identifiable { |
当点击 add 按钮时,“汤姆”猫出现在 List 的首位。让我们用 SwiftUI Instruments 来查看当前视图的更新次数

因为 PetRow Identity 的不稳定性,导致 PetRow 的 body 被执行多次。如果放在一个大型项目中,可能会导致 App 出现卡顿。
使用计算属性生成 UUID 的方式遵循 Identifiable 会导致视图 Identity 不稳定,所以作如下修改,以提供稳定的 Identity。
1 | struct Pet: Identifiable { |
可以再次运行 SwiftUI Instruments 来查看当前视图的更新次数:

PetRow 的 body 被执行的次数锐减至 6 次!原本已有 5 个宠物,再加上新增的“汤姆”猫,总计 6 次,与预期相符。
TreatJar 示例
再看这个稍微复杂一点的 例子。Treat Jar View 展示的是给宠物准备的零食。如果有零食过期,那就置灰。

我们借助于 ExpirationModifier 来实现置灰的效果。
1 | var body: some View { |
1 | struct ExpirationModifier: ViewModifier { |
此时执行点击左上角 Expire 按钮来模拟所有的零食过期。SwiftUI Instruments 给出的视图更新次数如下:

代表两种零食的 TreatCell 创建了两次,但实际上零食过期后,只需置灰即可,不需要重新创建 TreatCell。
1 | struct ExpirationModifier: ViewModifier { |
使用了 ExpirationModifier 之后, ForEach 中的 Content View 的 Identity 属于 Structural Identity
1 | // ForEach 中的 Content View |
根据之前的经验,我们可以通过这种方式来优化,即去除 if 分支:
1 | struct ExpirationModifier: ViewModifier { |
运行 SwiftUI Instruments,再次点击左上角 Expire 按钮,可以看到:

TreatCell.body 没有再被执行过了!所以在 View 中使用 if 分支时,可以停下来问自己:我是要多个视图还是某一视图的两种状态呢?引入不必要的分支,可能会导致性能下降,甚至视图的状态丢失。
Dependency

(本节不是本文讨论的重点,于是我放上 AI 对此的总结)
- 依赖的含义:视图的输入(属性、@State/@Binding/@Environment/ObservableObject 等)。依赖一旦
变更,对应视图就必须重新计算 body。 - 依赖关系的结构:虽然视图层级像树,但同一个依赖可被多处使用,整体更像一张依赖图。某个依赖变
化时,SwiftUI 只让依赖它的视图重建,而不是全量刷新。 - 身份在其中的作用:视图的身份(显式 id 或结构身份)决定依赖变化如何被路由到具体节点;身份稳
定,就能更好地保持状态和避免多余重建。 - 选择稳定且唯一的标识:数据驱动的视图(ForEach 等)需要稳定、唯一的 id。随机 id 或数组下标会让身份漂移,造成闪烁、错误动画或状态丢失。
- 精简条件分支:多余的 if/switch 会制造额外的身份节点,导致重复重建。能用“惰性”修饰符(如条件
放进 opacity,1 时无影响)折叠条件时,更新会更集中高效。 - 更新流程心智模型:依赖变化 → 标记相关视图 → 重新求 body 生成新值;值本身是短暂对比用的,持
久状态与视图身份绑定并随身份存续。
回顾
还记得文章开头我提及的奇怪 BUG 吗?这是能复现的 demo,而且在 iOS 17.1.2 上能稳定复现。
1 | typealias DetailState = DetailViewModel.State |
核心就在于通过 Navigator push Detail 页面时,DetailState 的 explicit identity 不稳定。每次求值 body 都会重新访问 State.id,计算属性每次生成新的 UUID,导致 Identifiable 身份变化。SwiftUI 认为当前 NavigationStack 顶端元素变了,于是销毁旧的 Detail 页面并创建新的 Detail 页面。
1 | Button("Show Detail") { |
运行 Swift Instruments 来看看视图的变化次数:

从上图可以看出:在短短几秒之内,DetailView 更新了将近 5000 次,SwiftUI 内部的 NavigationStack 更新了将近一万五千次,整个 app 出现了严重的卡顿!