SwiftUI 作为一个响应式框架,其视图需要随着状态的变化而更新。这就要求它依赖于某种机制来追踪这些状态变化。Observation 框架核心目标是为 SwiftUI 引入一个全新的观察机制,以此来提升 SwiftUI 应用的性能。在引入 Observation 框架之前,SwiftUI 实际上已经有了两套不同的观察机制…
@State
进行声明,而这些状态大多是值类型。对于此类状态,SwiftUI 能够自行进行观察。Combine
框架提供的“发布者-订阅者”模式来实现观察。但这种观察模式在当前存在导致大量视图无效更新等问题。参考链接:
实际上,Combine 观察模式在当前的 SwiftUI 中存在一些问题。它会导致大量视图无效更新…
final class MyObservableObject: ObservableObject {
@Published var name: String
@Published var age: Int
// 更多属性...
}
extension MyObservableObject {
var objectWillChange: Self.ObjectWillChangePublisher // 内部发布者实例,视图将订阅这个发布者
}
当使用 Combine 框架,并声明类型为 ObservableObject
的可观察对象时,首先需要的是符合 ObservableObject
协议。这个协议会在类中创建一个发布者实例。每当有属性被 @Published
修饰符标记的变量发生改变时,@Published
会触发类中的这个发布者,向所有订阅者发送通知。然而,因为这些通知不包含改变的具体信息,订阅者无法得知是哪个 @Published
标记的属性发生了变化。
在 SwiftUI 中,与可观察对象相关联的视图就是这些订阅者。即使是可观察对象中极小的一部分状态发生变化,也会导致所有相关联的视图进行更新,进而造成了大量的无效视图更新,严重影响了应用的性能。
// 例如:这里有一个具有两个属性的可观察对象
final class Store: ObservableObject {
@Published var name = "肥肥"
@Published var age = 5
}
// 在两个不同的子视图中,我们分别引用了这个对象,并且每个视图只使用了其中一个属性:
struct NameView: View {
@ObservedObject var store: Store
var body: some View {
let _ = print("NameView Update")
Text(store.name)
}
}
struct AgeView: View {
@ObservedObject var store: Store
var body: some View {
let _ = print("AgeView Update")
Text(store.age, format: .number)
}
}
由于 NameView
视图与 store
实例建立了联系(即响应该实例发出的通知),因此,即便是 age
属性发生变化时,它也会被更新。这就揭示了当前观察机制的不足之处。正是基于这个考虑,苹果公司推出了 Observation 框架。
Observation 框架为开发者提供了两个主要工具:@Observable
宏和 withObservationTracking
函数。宏在 Swift 5.9 中引入,旨在减轻开发者的负担,避免手动编写重复的代码,从而提高开发效率。
@Observable
首先在引用类型声明中引入一个“观察协调器”—— ObservationRegistrar
实例。这个协调器负责维护可观察属性与观察者之间的联系。这有点类似于 ObservableObject 协议为类添加发布者的过程,但原理和工作机制完全不同。@Observable
会把存储属性转化为计算属性,确保它们的存储操作完全由观察协调器管理,这样可以整合观察逻辑,这在某种程度上类似于 @Published
属性包装器的工作方式。withObservationTracking
来构建观察。这个函数要求提供两个闭包:所有需要被观察的属性都必须在 apply 闭包中出现并被读取其值。当被观察的属性即将发生变化时,框架会调用 onChange
闭包,完成“观察-回调”的完整流程。通过这种方式,Observation 提供了对属性的细粒度观察能力,解决了仅能观察整个实例而导致的精度不足问题,这是其解决 Combine 观察机制中存在问题的根本方案。withObservationTracking
来手动构建观察,因为 SwiftUI 已经在其视图评估逻辑中集成了观察操作。这种观察逻辑看起来独特,实际上是为满足 SwiftUI 的特定需求而精心设计的。它形成了一个完美的闭环:从“创建观察”(将视图的 body 放入 apply),到“状态变化”,再到“视图更新”(调用 onChange 闭包),最后“重新创建观察”(重复之前的步骤),这一系列操作紧密相连,适应了 SwiftUI 的渲染和更新机制。
Observation 框架是 SwiftUI 5 中的一个重大创新,它完全改变了 SwiftUI 的响应式基础。Observation 框架带来了许多好处:
State
和 Environment
,替代了以前的 StateObject
和 EnvironmentObject
@Observable
宏,而不需要使用 @Published
属性包装器来注释每个可观察属性<aside> 💡 如果您的应用当前因观察精度不足而遭受大量无效视图更新和性能下降的问题,仅需将其转换为 Observation 方式,便可迅速看到明显的改善。想要深入了解 Observation 框架的具体使用方法和更多细节,推荐阅读 深度解读Observation——SwiftUI性能提升的新途径。
</aside>
Observation 框架所提供的观察机制展现了几个独特的特点:
onChange
闭包,允许开发者在变化发生前做出响应onChange
闭包被触发,相应的观察操作就会结束onChange
闭包,并结束这次观察Observation 提供了一种全新的构建和组织应用状态的方式。现在可以将一个可观察对象嵌套在另一个对象中,实现以前难以构建的状态关系。在以前 Combine 框架下,尝试实现这种嵌套会面临很大的挑战,因为 @Published
属性不支持观察引用类型内部的变化。
@Observable
class A {
var b = 1
}
@Observable
class B {
var b = 1
//将一个可观察对象作为另一个可观察对象的属性
var a = A()
}
通过 Observation,视图的更新变得更加精准和高效。它不仅精确到属性级别,而且还精确到对属性的具体操作方式。
只有当视图实际读取可观察对象的属性(即触发其 getter 方法)时,才会建立观察关系。如果只是赋值或调用可观察对象中的方法,而不触发属性的读取,就不会与视图建立观察联系。这种精准观察能力大大减轻了开发负担,能更放松的去设计状态。
以往在基于 Combine 的实现中,只要将可观察对象引入到视图中(比如使用 @EnvironmentObject),即使 body 中没有使用任何属性或方法,视图仍然需要响应可观察对象实例的变化。
// 例如以下示例代码:
struct A: View {
let store: Store
var body: some View {
Text(store.name) // 读取属性,就会创建关联
}
}
struct B: View {
let store: Store
var body: some View {
Button("Update"){
store.name = "hello" // 只是赋值,未触发getter方法,不会建立观察关系
}
}
}
Observation 框架的另一个关键优点是:前向兼容性(Back-porting)。
import SwiftUI
import Combine
// 1.创建一个符合 ObservableObject 协议的类
class UserData: ObservableObject {
// 2.使用 @Published 标注可观察属性
@Published var username = "Guest"
@Published var age = 25
}
//【根页面】
struct ContentView: View {
// 3.使用时用 @ObservedObject 实作
@ObservedObject var userData = UserData()
var body: some View {
VStack {
Text("Username: \\\\(userData.username)")
Text("Age: \\\\(userData.age)")
Button("Update Username") {
userData.username = "NewUser"
}
Button("Increase Age") {
userData.age += 1
}
}
// 4.使用 .environmentObject 方法将 userData 注入到环境中
.environmentObject(userData)
}
}
//【其他页面】
struct DetailView: View {
// 5.应用的其他页面通过 @EnvironmentObject 获取环境对象
@EnvironmentObject var userData: UserData
var body: some View {
VStack {
Text("Detail View")
Text("Username: \\\\(userData.username)")
Text("Age: \\\\(userData.age)")
Button("Update Username") {
userData.username = "UpdatedUser"
}
}
}
}
要将依赖 Combine
的可观察机制迁移到依赖 Observation
框架的机制,需要进行几项关键的调整:
import SwiftUI
import Observation
// 1.移除 ObservableObject 协议,直接使用 @Observable 宏来使整个类的属性具有观察性
@Observable class UserData {
// 2.移除 @Published:新机制中所有存储属性都会自动被观察,不需要额外的 @Published 标记
var username = "Guest"
var age = 25
}
//【根页面】
struct ContentView: View {
// 3.用 @State 来替换原本的 @StateObject
@State var userData = UserData()
var body: some View {
VStack {
Text("Username: \\\\(userData.username)")
Text("Age: \\\\(userData.age)")
Button("Update Username") {
userData.username = "NewUser"
}
Button("Increase Age") {
userData.age += 1
}
}
// 4.使用新的环境注入方法( 从 .environmentObject 变为了 .environment)
.environment(userData)
}
}
//【其他页面】
struct DetailView: View {
// 5. 访问环境对象的方法从 @EnvironmentObject 变为 @Environment,并且需要传递类型 UserData.self
@Environment(UserData.self) var userData
var body: some View {
VStack {
Text("Detail View")
Text("Username: \\\\(userData.username)")
Text("Age: \\\\(userData.age)")
Button("Update Username") {
userData.username = "UpdatedUser"
}
}
}
}