在 SwiftUI 中,body 的更新并非全量替换,而是一个高度优化的**“三阶过滤”**过程。理解这些计算类型,是解决性能卡顿的关键。
1. SwiftUI 触发的三种重新计算
当一个状态(State)改变时,SwiftUI 会按顺序执行以下评估:
A. 逻辑评估(Body Evaluation)
这是最表层的计算。SwiftUI 执行你的 body 闭包,生成一套全新的 View 结构体值。
- 开销:取决于你在
body里写了多少逻辑(如filter、map或复杂的字符串格式化)。 - 触发频率:只要观察的状态变了,就会触发。
B. 差异对比(Attribute Graph Diffing)
SwiftUI 拿到新的 View 树后,会将其与内存中现有的 Attribute Graph 进行对比。
- 机制:它会检查视图的 Identity(身份) 。如果身份没变,它会对比属性(如颜色、字体、绑定值)。
- 结果:如果属性完全一致,计算到此停止,不会触发布局和渲染。
C. 布局与渲染(Layout & Rendering)
如果 Diff 发现属性确实变了,SwiftUI 才会执行最后一步。
- 动作:调用
sizeThatFits、计算几何位置、提交给 GPU 绘图。 - 开销:这是最昂贵的一步,涉及 Core Animation 的图层提交。
2. 如何避免不必要的刷新?(防御式策略)
优化的核心目标是:尽可能在第一或第二阶段就拦截掉更新。
策略一:收窄状态观察范围(Dependency Minimization)
不要让大型对象污染子视图。
- 差:传递整个
UserViewModel给一个小头像组件。任何用户属性变了,头像都会重绘。 - 好:只传递
avatarURL字符串。 - 原理:SwiftUI 会自动追踪
body里实际使用了哪些属性(Dependency Tracking)。
策略二:利用 EquatableView 进行强制拦截
如果你的 View 结构比较复杂,但输入参数变动不频繁,可以使用 .equatable()。
Swift
struct ComplexRow: View, Equatable {
let id: Int
let title: String
// 只有 id 和 title 变了,才会跑 body
static func == (lhs: ComplexRow, rhs: ComplexRow) -> Bool {
lhs.id == rhs.id && lhs.title == rhs.title
}
var body: some View { ... }
}
// 调用处
ComplexRow(id: 1, title: "Hello").equatable()
策略三:保持视图层级的稳定性
避免频繁改变视图的结构(如 if-else 切换不同的 View 类型)。
- 优化:尽量通过属性(如
.opacity()或.offset())来改变视觉效果,而不是直接移除/添加视图。 - 原因:改变结构会导致 Structural Identity 破裂,迫使 SwiftUI 放弃 Diff 直接进行昂贵的重建。
策略四:将计算移出 body
body 应该是只读的。
- 防御做法:所有的复杂计算(如数组排序、日期转换)都应该放在
ViewModel的属性中,或者利用memoization(缓存结果)。body只负责读取最终的字符串或模型。
3. 性能调试的小技巧
如果你怀疑有不必要的刷新,可以使用这个“后门”进行监测:
Swift
var body: some View {
// 在控制台打印出究竟是哪个状态触发了本次刷新
let _ = Self._printChanges()
return VStack { ... }
}
总结:优化清单
- 状态分片:一个视图只依赖它显示的那个字段。
- 避免
AnyView:它会破坏 Diff 性能。 - 常量化:不需要改变的视图(如背景、装饰图标)可以提取为
static let属性,SwiftUI 会视其为常量而永远不重绘。
12-10. [SwiftUI] Which types of recomputations are triggered by a body update? How to avoid unnecessary refreshes?
In SwiftUI, a body update is not a brute-force replacement of the entire UI. Instead, it is a highly optimized "three-stage filtering" process. Understanding these types of computation is key to solving UI jank and performance bottlenecks.
1. The Three Stages of SwiftUI Recomputation
When a state changes, SwiftUI evaluates the view hierarchy in the following order:
A. Logic Evaluation (Body Evaluation)
This is the surface-level calculation. SwiftUI executes your body closure to generate a brand-new tree of View struct values (lightweight descriptions).
- Cost: Depends on how much logic you've placed inside the
body(e.g.,filter,map, or complex string formatting). - Trigger Frequency: High. Triggered whenever an observed state or dependency changes.
B. Attribute Graph Diffing
Once SwiftUI has the new View tree, it compares it against the existing Attribute Graph maintained in memory.
- Mechanism: It checks the Identity of the views. If the identity hasn't changed, it compares specific attributes (colors, fonts, bindings).
- Result: If the attributes are identical to the previous snapshot, the process stops here. No layout or rendering is triggered.
C. Layout & Rendering
If the Diffing stage detects actual changes in attributes or structure, SwiftUI performs the final, most expensive step.
- Action: Calls
sizeThatFits, calculates geometric positions (Layout), and submits drawing commands to the GPU (Rendering). - Cost: Very high. This involves Core Animation layer commits and pixel drawing.
2. How to Avoid Unnecessary Refreshes (Defensive Strategies)
The core goal of optimization is to intercept the update at the first or second stage whenever possible.
Strategy 1: Narrow the Observation Scope (Dependency Minimization)
Do not let large, "noisy" objects pollute subviews.
- Bad: Passing an entire
UserViewModelto a small Avatar component. If the user's "last login time" changes, the Avatar unnecessarily re-evaluates itsbody. - Good: Pass only the
avatarURLstring. - Principle: SwiftUI automatically tracks exactly which properties are read inside a
body(Dependency Tracking).
Strategy 2: Use EquatableView for Manual Interception
If your view structure is complex but its input parameters change infrequently, use .equatable().
Swift
struct ComplexRow: View, Equatable {
let id: Int
let title: String
// Body only runs if id or title actually changes
static func == (lhs: ComplexRow, rhs: ComplexRow) -> Bool {
lhs.id == rhs.id && lhs.title == rhs.title
}
var body: some View { ... }
}
// Usage
ComplexRow(id: 1, title: "Hello").equatable()
Strategy 3: Maintain Structural Stability
Avoid frequently changing the view's hierarchy (e.g., using if-else to swap between different View types).
- Optimization: Use modifiers like
.opacity()or.offset()to change visuals rather than adding/removing views from the tree. - Reason: Changing the structure breaks Structural Identity, forcing SwiftUI to skip Diffing and perform an expensive full rebuild.
Strategy 4: Move Calculations out of body
The body property should be treated as a read-only description.
- Defensive Practice: All complex calculations (sorting arrays, date conversions) should live in the
ViewModelor be pre-processed. Thebodyshould only be responsible for reading the final strings or models.
3. Pro-Tip: Debugging with _printChanges()
If you suspect unnecessary refreshes, use this built-in debugging tool to see exactly which state triggered the update:
Swift
var body: some View {
// Prints the specific property that caused the refresh to the console
let _ = Self._printChanges()
return VStack { ... }
}
Summary: Optimization Checklist
- State Sharding: Ensure a view only depends on the specific fields it displays.
- Avoid
AnyView: Type erasure breaks the Diffing engine's performance. - Constant Extraction: Extract non-changing views (backgrounds, decorative icons) into
static letproperties. SwiftUI treats these as constants and will never re-evaluate them.