在 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) }
}
优势: 当主题模式切换时,UserNameView 的 body 根本不会被触发。它就像在一个嘈杂的房间里带上了降噪耳机,只听它关心的信息。
3. 为什么粒度大比大小更危险?
- Diff 成本:SwiftUI 虽然 Diff 很快,但如果你有几千个视图层级,每一次无意义的
body调用都会累积成毫秒级的卡顿。 - 动画中断:粗粒度的状态更新有时会触发不必要的布局计算,导致正在进行的平滑动画产生微小的跳动。
- 计算属性的陷阱:如果你在
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 的第一步。