SwiftUI 的视图 Diff(差异化对比)是其高性能的核心。与传统 Web 开发中常见的虚拟 DOM Diff 不同,SwiftUI 的 Diff 更加智能,它依赖于强类型系统和身份识别(Identity) 。
1. 视图 Diff 的核心:Identity(身份)
SwiftUI 并不对比视图的每一个属性,而是首先确定“你还是不是原来的你”。
显式身份 (Explicit Identity)
通过 .id() 手动指定的身份。
- 机制:如果
id变了,SwiftUI 认为这是全新的视图,直接销毁旧的并重建。 - 应用:常用在
ForEach或滚动跳转。
结构身份 (Structural Identity)
通过视图在代码树中的位置确定的身份。
- 机制:SwiftUI 会为每一个视图生成一个路径(如:
VStack -> Section -> Toggle)。 - 防御式理解:只要位置没变,即便属性变了,SwiftUI 也认为它是同一个视图,只会更新属性。
2. Diff 的执行流程
当 @State 或 @ObservedObject 发生变化时,会触发以下流程:
-
失效(Invalidation) :关联的状态标记为“脏”,通知 SwiftUI 重新评估。
-
重绘 body:SwiftUI 调用相关视图的
body属性,生成一个新的值类型结构体。 -
属性图对比 (Attribute Graph Diff) :
- 对比新旧两个结构体。由于
View是轻量级结构体,对比开销极低。 - 如果身份(Identity)匹配,则仅提取属性差异。
- 对比新旧两个结构体。由于
-
提交更改:将差异打包,批量提交给底层的渲染引擎(如 Core Animation)。
3. 性能优化的核心策略
在声明式 UI 中,性能优化往往意味着**“减少不必要的 body 计算”**。
A. 状态本地化 (State Localization)
不要在根视图管理所有状态。
- 原理:状态改变会触发其拥有者及其子树的重新评估。将状态下沉到具体的子视图,可以极大收窄 Diff 的范围。
B. 避免使用 AnyView
AnyView 会擦除类型,导致 SwiftUI 无法追踪结构身份。
- 后果:每次重绘,
AnyView内部的视图都会被强行销毁并重建,导致动画丢失和 CPU 飙升。 - 替代方案:使用
@ViewBuilder或Group。
C. 使用 .equatable() 阻止多余重绘
如果某个视图的输入参数很复杂,但并不总是导致 UI 变化,可以让视图遵循 Equatable 协议。
- 机制:通过
.equatable()告诉 SwiftUI,如果新旧View结构体相等,就不要去跑body逻辑了。
D. 扁平化视图层级
虽然 SwiftUI 鼓励拆分小视图,但过深的嵌套(超过 10-15 层)会增加 Attribute Graph 的遍历负担。
- 策略:合理利用
Group或将纯装饰性的视图通过扩展(Extension)合并。
4. Diff 效率对比表
| 特性 | 影响范围 | 优化建议 |
|---|---|---|
改变 .id() | 最大 (销毁+重建) | 仅在需要重置状态时使用。 |
if-else 分支切换 | 中 (部分重建) | 尽量通过属性变化而非分支切换来改变 UI。 |
| 仅仅属性改变 | 低 (属性更新) | 性能最优,SwiftUI 会自动处理。 |
| 闭包中的复杂计算 | 取决于频率 | 移出 body,放在 ViewModel 中预处理。 |
总结:防御式 Diff 思维
在 SwiftUI 中, “少即是多” 。保持视图结构的稳定性,比手写复杂的 Diff 逻辑要有效得多。
提示:你可以通过在
body里添加let _ = Self._printChanges()来在控制台实时查看究竟是哪个状态触发了视图的重新渲染。