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()来在控制台实时查看究竟是哪个状态触发了视图的重新渲染。
12-4. [SwiftUI] How does SwiftUI implement View Diffing? What are the strategies for Performance Optimization?
SwiftUI's View Diffing (differential comparison) is the heart of its high performance. Unlike the Virtual DOM diffing common in web development, SwiftUI’s approach is more intelligent, leveraging its strong type system and Identity recognition.
1. The Core of View Diffing: Identity
SwiftUI doesn't just compare every attribute of a view; it first determines, "Are you still the same entity?"
Explicit Identity
This is an identity manually assigned using the .id() modifier.
- Mechanism: If the
idchanges, SwiftUI considers it a brand-new view, destroying the old one and rebuilding from scratch. - Application: Commonly used in
ForEachor to force-reset a component's state.
Structural Identity
This identity is determined by the view's position within the code's hierarchy.
- Mechanism: SwiftUI generates a path for every view (e.g.,
VStack -> Section -> Toggle). - Defensive Understanding: As long as the position hasn't changed, even if attributes change, SwiftUI views it as the same instance and only updates the specific properties.
2. The Diffing Execution Flow
When a @State, @ObservedObject, or @Bindable changes, the following flow is triggered:
-
Invalidation: The associated state is marked as "dirty," notifying SwiftUI to re-evaluate the view.
-
Body Re-evaluation: SwiftUI calls the
bodyproperty of the affected views, generating a new value-type struct description. -
Attribute Graph Diffing:
- It compares the new struct with the previous one. Because
Viewis a lightweight struct, the overhead is minimal. - If the Identity matches, it only extracts the attribute differences (e.g., a color change).
- It compares the new struct with the previous one. Because
-
Commit Changes: The differences are batched and submitted to the underlying rendering engine (like Core Animation or UIKit).
3. Core Strategies for Performance Optimization
In declarative UI, optimization usually means "reducing unnecessary body calculations."
A. State Localization
Avoid managing all state in the root view.
- Principle: A state change triggers a re-evaluation of its owner and the entire subtree. By pushing state down to the specific subview that needs it, you drastically narrow the scope of the Diff.
B. Avoid Using AnyView
AnyView performs type erasure, which prevents SwiftUI from tracking Structural Identity.
- Consequence: Every time a redraw occurs, the view inside
AnyViewis forcibly destroyed and rebuilt, leading to broken animations and high CPU usage. - Alternative: Use
@ViewBuilderorGroup.
C. Use .equatable() to Prevent Redundant Redraws
If a view has complex input parameters that don't always result in a UI change, you can make the view conform to the Equatable protocol.
- Mechanism: By wrapping the view in
.equatable(), you tell SwiftUI: "If the new and oldViewstructs are equal, don't even bother running thebodylogic."
D. Flatten the View Hierarchy
While SwiftUI encourages small, reusable views, extremely deep nesting (over 10–15 layers) increases the traversal burden on the Attribute Graph.
- Strategy: Use
Groupto logically organize views without adding rendering layers, or merge purely decorative views using Extensions.
4. Diff Efficiency Comparison Table
| Action | Impact Scope | Optimization Advice |
|---|---|---|
Changing .id() | Maximum (Destroy + Rebuild) | Use only when a total state reset is required. |
if-else Branch Swap | Medium (Partial Rebuild) | Prefer property changes over branch swaps for minor UI tweaks. |
| Simple Attribute Change | Low (Property Update) | Optimal performance; SwiftUI handles this natively. |
| Complex Logic in body | Varies | Move calculations to the ViewModel or pre-process them. |
Summary: The Defensive Diff Mindset
In SwiftUI, "Less is More." Maintaining a stable view structure is far more effective than trying to manually write complex diffing logic.
Pro Tip: You can see exactly which state triggered a view refresh by adding
let _ = Self._printChanges()inside yourbodyproperty. This will print the source of the change to the console.