一、结论先行
在 SwiftUI/TCA 中,State 变化频率远比 State 大小更危险,
因为每次 State 改变都会触发 View 失效、body 重新计算、diff、ARC 调用——这些开销都是按“频率累积”的,而不是按 State 字节数线性增加”。
换句话说:大 State 偶尔改没问题,小 State 高频改也能炸。
二、渲染角度(Render)
1️⃣ SwiftUI 的渲染机制
- State 改变 → SwiftUI 标记依赖该 State 的 View 为 dirty
- body 被重新计算(全量 body 重新执行)
- diff 旧 / 新 View → 更新渲染树
- GPU / CALayer 更新
2️⃣ 频率 vs 大小
| 情况 | 描述 | 开销来源 |
|---|---|---|
| 大 State 偶尔改 | 比如一次 API 返回 1000 条数据 | body 计算多,但只发生一次,短暂开销可接受 |
| 小 State 高频改 | 用户输入、动画状态、滚动索引 | 每改一次 → body、diff、渲染、UI 更新 → CPU/GPU 长期占用 |
关键点:渲染开销是“频率累积”,而不是一次性。
三、ARC / 内存管理角度
1️⃣ State 变化会创建新对象
- TCA / Reducer 设计不可变 State → 每次修改生成新 State
- SwiftUI View 值类型 → 每次 body 生成新的 View 结构
- ARC 会管理这些对象的生命周期
2️⃣ 高频改导致的压力
每一次 State 改变 → 新对象生成 + 老对象释放
- CPU:ARC retain/release 调用频繁
- 内存:如果 body 内部有缓存对象或闭包引用,会产生峰值
- 高频输入 → 内存短时间飙升,可能触发 GC /回收压力 → UI 卡顿
小 State 高频改,比大 State 偶尔改,更容易触发 ARC 负担。
四、Diff 角度(最直观)
1️⃣ SwiftUI diff 本质
-
每次 body 被调用 → 生成新的 View 树
-
SwiftUI 对比 old/new tree:
- Type
- Identity (
id/StateObject) - 子 View 数量
-
发现不同 → 更新渲染树
2️⃣ 高频修改 vs 大 State
- 大 State 偶尔改 → diff 一次,渲染一次 → CPU 可以接受
- 小 State 高频改 → diff 每次都走一遍 → CPU 消耗累积
Tip: 即使 State 本身只有几个字段,如果它触发的 body 很复杂(列表、ForEach、动画),频率高就会成为瓶颈。
五、工程直觉示例
1️⃣ 高频输入导致性能问题
@State var searchText = ""
var body: some View {
List(filteredItems(searchText)) { item in
Text(item.name)
}
}
- 用户每敲一个字符 →
searchText改一次 → List body 全部重算 - CPU 高占用 → 滚动卡顿
- ARC 高频分配释放对象
- Diff 每次计算整个 List
2️⃣ 大 State 偶尔改(安全)
@State var users: [User] = []
// API 返回一次 1000 条
users = fetchUsers()
- body 计算 List 1000 次,但只触发一次
- CPU 峰值短暂 → 用户感受几乎没有问题
六、总结:为什么频率比大小更危险
| 角度 | 原因 |
|---|---|
| 渲染 | 每次变化 → body 重算 + 渲染树 diff;频率高 → 累积开销 |
| ARC | 每次变化 → 新 State/Value 对象生成 + 老对象释放;频率高 → CPU / 内存压力累积 |
| Diff | 每次变化 → SwiftUI 比较旧/新 View;频率高 → diff 成本累积 |
工程口诀:
“State 大不怕,State 频繁改才要命;粒度越小频率越高,优化越重要。”
七、工程实践建议
-
下沉 State 粒度
- 高频变化状态放在最小 View 或 Row 层
- 避免全局 / 父 View 直接承载
-
派生状态即时计算 / memoization
- 不存 Derived State 在 @State 中
- 减少无谓的频繁改动
-
使用
@StateObject/ ViewModel 缓存复杂计算- 减少 body 中昂贵计算
-
尽量 batch 高频 Action
- 例如输入防抖、动画状态统一 tick 更新