6-17.【架构设计】SwiftUI/TCA 中,为什么“State 变化频率”比“State 大小”更危险?请从 渲染 + ARC + diff 的角度解释。

3 阅读3分钟

一、结论先行

在 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 频繁改才要命;粒度越小频率越高,优化越重要。”


七、工程实践建议

  1. 下沉 State 粒度

    • 高频变化状态放在最小 View 或 Row 层
    • 避免全局 / 父 View 直接承载
  2. 派生状态即时计算 / memoization

    • 不存 Derived State 在 @State 中
    • 减少无谓的频繁改动
  3. 使用 @StateObject / ViewModel 缓存复杂计算

    • 减少 body 中昂贵计算
  4. 尽量 batch 高频 Action

    • 例如输入防抖、动画状态统一 tick 更新