12-4.【SwiftUI】SwiftUI 是如何实现视图 diff 的?性能优化有哪些策略?

0 阅读3分钟

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 发生变化时,会触发以下流程:

  1. 失效(Invalidation) :关联的状态标记为“脏”,通知 SwiftUI 重新评估。

  2. 重绘 body:SwiftUI 调用相关视图的 body 属性,生成一个新的值类型结构体。

  3. 属性图对比 (Attribute Graph Diff)

    • 对比新旧两个结构体。由于 View 是轻量级结构体,对比开销极低。
    • 如果身份(Identity)匹配,则仅提取属性差异。
  4. 提交更改:将差异打包,批量提交给底层的渲染引擎(如 Core Animation)。


3. 性能优化的核心策略

在声明式 UI 中,性能优化往往意味着**“减少不必要的 body 计算”**。

A. 状态本地化 (State Localization)

不要在根视图管理所有状态。

  • 原理:状态改变会触发其拥有者及其子树的重新评估。将状态下沉到具体的子视图,可以极大收窄 Diff 的范围。

B. 避免使用 AnyView

AnyView 会擦除类型,导致 SwiftUI 无法追踪结构身份。

  • 后果:每次重绘,AnyView 内部的视图都会被强行销毁并重建,导致动画丢失和 CPU 飙升。
  • 替代方案:使用 @ViewBuilderGroup

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() 来在控制台实时查看究竟是哪个状态触发了视图的重新渲染。