12-6.【SwiftUI】SwiftUI 中状态粒度为什么比状态大小更重要?举例说明。

0 阅读3分钟

在 SwiftUI 的性能调优中,有一条被开发者奉为圭臬的准则: “不要担心状态有多大,要担心状态的影响范围有多广。”

简单来说,**状态粒度(Granularity)**决定了哪些视图需要被重新计算。如果粒度太粗,哪怕只是改了一个像素的颜色,也可能导致整个复杂的页面树被彻底 Diff 一遍。


1. 核心逻辑:失效的传播范围

在 SwiftUI 中,状态变化会触发其**观察者(Observers)**的 body 重新评估。

  • 状态大小(Size) :指的是数据占用了多少内存。在现代设备上,存储一个拥有 1000 个字段的 struct 几乎不消耗资源,因为 Swift 的值类型拷贝是高效的。
  • 状态粒度(Granularity) :指的是状态变化时**“失效表面积”**的大小。如果一个“上帝对象(God Object)”承载了太多无关的数据,任何细微的抖动都会导致大面积的 UI 失效。

2. 举例说明:粗粒度 vs. 细粒度

假设我们在开发一个个人中心页面,包含“用户信息”和“系统设置”。

方案 A:粗粒度(上帝对象)

我们将所有数据塞进一个对象。

Swift

class AppState: ObservableObject {
    @Published var userName: String = "Gemini"
    @Published var isDarkMode: Bool = false
    @Published var lastLogin: Date = Date()
}

struct ProfileView: View {
    @ObservedObject var state: AppState // 观察了整个上帝对象
    
    var body: some View {
        VStack {
            Text(state.userName) // 依赖 A
            Toggle("黑夜模式", isOn: $state.isDarkMode) // 依赖 B
        }
    }
}

问题点: 即使 lastLogin(最后登录时间)在后台每秒更新一次,由于 ProfileView 观察的是整个 state,SwiftUI 必须每秒重新计算一遍 body。即使 UI 上根本没显示时间,性能也被白白浪费了。


方案 B:细粒度(分而治之)

我们将状态拆分,或者只传递必要的绑定。

Swift

struct ProfileView: View {
    var body: some View {
        VStack {
            UserNameView()    // 仅感知名字变化
            ThemeToggleView() // 仅感知模式变化
        }
    }
}

struct UserNameView: View {
    @State private var name: String = "Gemini" // 粒度极细
    var body: some View { Text(name) }
}

优势: 当主题模式切换时,UserNameViewbody 根本不会被触发。它就像在一个嘈杂的房间里带上了降噪耳机,只听它关心的信息。


3. 为什么粒度大比大小更危险?

  1. Diff 成本:SwiftUI 虽然 Diff 很快,但如果你有几千个视图层级,每一次无意义的 body 调用都会累积成毫秒级的卡顿。
  2. 动画中断:粗粒度的状态更新有时会触发不必要的布局计算,导致正在进行的平滑动画产生微小的跳动。
  3. 计算属性的陷阱:如果你在 body 里有一些复杂的格式化逻辑(如 DateFormatter),粗粒度状态会导致这些昂贵的逻辑被高频、无意义地重复执行。

4. 进化:Observation 框架 (Swift 5.9+)

在旧的 ObservableObject 中,只要有一个 @Published 属性变了,所有观察者都会刷新。 而新的 Observation 框架(使用 @Observable 宏)实现了字段级的追踪

  • 以前:改了 state.a,监听 state 的 View 就会刷新。
  • 现在:即使你传入了整个对象,如果你的 View 只读取了 state.a,那么 state.b 的改变绝对不会触发该 View 的刷新。这就是框架层面对“细粒度”的极致支持。

5. 防御式设计建议

  • 状态下沉:如果一个状态只在某个按钮里用,就不要把它放在父视图的 @State 里。
  • ViewModel 拆分:不要让一个 ViewModel 负责整个 TabBar 的五个页面,按功能模块拆分。
  • 善用计算属性:在 View 层级中,尽量通过具体的参数传递数据,而不是传递整个复杂的 Model 对象。

总结:在 SwiftUI 中, “数据大”只是占内存,“粒度粗”才是毁性能。 保持状态的“私有化”和“局部化”,是写出丝滑 UI 的第一步。