2026·06·21 · 阅读约 9 分钟 · Technical
从徒手刷 UI 到一个 @Observable:移动开发状态绑定的演进
更新于
「前情提要」
说起来惭愧,转行做软件开发,今年是第12个年头了。以前面试的时候回答不上技术问题,总以自己不是计算机科班出身为借口。但是,12年过去,大学都够读3回了,「不是计算机科班出身」,渐变成不太靠谱的借口。
如果硬是要甩锅,或许可以这样解释,我是一个「视觉学习者」,面对浩如烟海、每年更新的技术文档,我的大脑理所当然地拒绝,我是可以理解——甚至有点同情我的大脑。加之我们的教育并没有教我们「Learning How to Learn」,所以12年间「学习」过的技术知识,如水过鸭背。
不过到了AI时代,事情发生了微妙的变化,以NotebookLM为代表,AI可以轻易把「枯燥」的文字,转换为生动的音视频,让学习者轻松完成第一次的吸收;遇到问题,也可以随时随地「请教」大模型——一个集人类知识之大成,(理论上)全知全能的老师,当然,「幻觉」问题暂时按住不表,大方向已定——人类离不开大模型。
正因为「知识」触手可及、前所未有地廉价——一台手机+一个Wi-Fi,可以获取任何「知识」,「学习」「知识」变得不那么重要。所以,在AI时代,「学习」在某种程度上变成一种兴趣,可以演变为一种纯粹为了获得多巴胺的行为,就像健身一样——你健身的目的,并不是为了有朝一日可以像武松一样徒手打虎,纯粹是为了满足自己——获得多巴胺。
跑题了,言归正传,这次想小结一下接触「移动开发」以来的一些历史,从iOS OC时代的徒手写UI、数据更新后更新UI,到iOS最新的Observation,用一个@Observable宏就搞定一切,这一路下来,是如何演进的。
之前看objc的《App Architecture》,说App的本质,就是UI + Model,UI发出动作,触发Model的更新,Model更新后,又触发UI的更新,如此循环往复,App就是这样一直运行。

最近的项目,是用Flutter做的,用到Riverpod,第一次接触;而且Riverpod从1.0一路演进到现在的3.0,公司项目里新旧写法混在一起,一时之间无从下手。于是想重新捋一遍——又因为最熟悉的还是iOS,索性顺便做了对比。
命令式与手动观察者年代/The Imperative Era
2008年起,iPhone刚发布没多久,移动开发还处在原始的「命令式」与「手动观察」年代,写UI,需要明确(命令式)写出UI(的每个细节);触发动作后,也要手动更新UI,不可谓不原始。
- (void)didTapMultiply {
// 命令式数据流:Controller 主动调用 Model,拿到结果后**亲手**刷新 View。
// 对照Swift最新的 @Observable 版:那边只需 `viewModel.multiply()`,result 变化由框架自动驱动 UI。
__weak typeof(self) weakSelf = self;
[self.model multiplyWithCompletion:^(NSString *result) {
weakSelf.resultLabel.text = result; // ← 手动刷新 UI,这一行是 MVC 的灵魂也是负担
}];
}
如果要实现监听Model变化,自动更新UI,需要手动实现,通过KVO或者notification:
// 手动实现监听
// ★ 订阅:监听 model.result 的变化(对照 SwiftUI body 里读 viewModel.result 自动建立的订阅)。
[self.model addObserver:self
forKeyPath:@"result"
options:NSKeyValueObservingOptionNew
context:kResultContext];
// Model有变化,自动回调,从而(自动)更新UI
// ★ KVO 回调:result 一变就自动进到这里,我们刷新 UI。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if (context == kResultContext) {
self.resultLabel.text = change[NSKeyValueChangeNewKey];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// 发出「动作」,触发Model更新
- (void)didTapMultiply {
[self.model multiply]; // 只触发;结果回来后由 KVO 自动刷新(无需手动设 label)
}
- (void)dealloc {
// 需要手动管理的年代,管理不当,会内存泄漏
// ★ 必须退订,否则 model 通知一个已释放的 observer 会崩溃(KVO 的经典坑)。
// 对照 @Observable / SwiftUI:订阅生命周期由框架自动管理,根本无需手写这步。
[self.model removeObserver:self forKeyPath:@"result" context:kResultContext];
}
后来演进到2012年的ReactiveCocoa——一个开创性第三方框架,把函数式响应式编程(FRP)带入iOS,启发了后来的一切。再演进到2015年的RxSwift,因为是第三方框架,这里掠过不表。
小结:这一时期,UI需要用代码一步步”指挥”出来。状态变化靠手动订阅 / 移除监听器、委托(delegate)和回调传递。最大隐患:忘记移除监听导致内存泄漏。第三方库开始引入”响应式”概念探路。
生命周期感知与响应式框架/Reactive & Lifecycle-Aware
到2019年,Apple推出自家的声明式UI框架SwiftUI,还有响应式框架Combine。人们可以用「ObservableObject / @Published」来实现「属性变化自动驱动视图重建」。
final class MultiplyViewModel: ObservableObject {
// ObservableObject 搭配 @Published,实现「属性变化自动驱动视图重建」/「属性可观察」
@Published private(set) var result = "-"
private let useCase: MultiplyUseCase
init(useCase: MultiplyUseCase) {
self.useCase = useCase
}
func multiply() async {
do {
result = try await useCase.multiply() // 赋值 → objectWillChange → 观察者重绘
} catch {
debugPrint("Error: \(error)")
}
}
}
// ViewModel的「注入」方式有三种
// 1 当前视图自己init
// 传递: 这一屏是根/拥有者
@StateObject private var viewModel = MultiplyViewModel(useCase: 实例化参数)
// 2 由父视图传入(在父视图init)
// 传递: 显式逐层传递
@ObservedObject var viewModel: MultiplyViewModel
// 3 上游某处init(通过.environmentObject(...) 注入)
// 传递: 跨多层共享(优势)
@EnvironmentObject private var viewModel: MultiplyViewModel
// UI直接使用即可
Text(viewModel.result)
BTW,真的忍不住吐槽写这个的Apple Engineer,「ObservableObject」、「@Published」、「@StateObject」、「@State」、「@ObservedObject」、「@EnvironmentObject」、「@Bindable」,用的人心智负担得有多大?这绝对是失败的设计,后面的「Observation Framework」出现便是例证。 冷知识:SwiftUI一开始只有「@ObservedObject」,没有「@StateObject」,后者是为了填坑(视图重建,「@ObservedObject」会让状态丢失)在2020年才加的。
而同时期,Flutter那边发生的事情:
- 2018.01,BLoC 模式:Paolo Soares 与 Cong Hui 在 DartConf 2018 提出,基于 Stream 的单向数据流,后在 I/O 2018 推广。
- 2018年底,Flutter 1.0 / 内置原语:setState、InheritedWidget、ChangeNotifier 等内置机制随框架诞生。
- 2019,Provider 成官方推荐:Rémi Rousselet 的 Provider(2018 年作)在 Google I/O 2019 被官方背书,封装 InheritedWidget。
小结:这一时期,官方开始提供「会自动管理订阅生命周期」的状态容器,彻底缓解了泄漏问题。
细粒度与现代化
时间先跳到2023,Apple演进出Observation Framework
// 「@Observable」 is all you need, 不再需要「ObservableObject」「@Published」
@Observable
final class MultiplyViewModel {
private(set) var result = "-"
private let useCase: MultiplyUseCase
init(useCase: MultiplyUseCase) {
self.useCase = useCase
}
func multiply() async {
do {
result = try await useCase.multiply()
} catch {
debugPrint("Error: \(error)")
}
}
}
// 「注入」端也改为
// 1 自己拥有
@State private var viewModel = MultiplyViewModel(useCase: 实例化参数)
// 2 外部传入,直接 let
let viewModel: MultiplyViewModel
// 3
@Environment(MultiplyViewModel.self) private var viewModel
// 另外,需要双向 binding 时 → @Bindable
Observation Framework让开发者的心智负担大大降低。
这样的演进,是为了解决几个痛点
- 旧机制观察粒度太粗,导致大量无谓的重绘
class UserModel: ObservableObject {
@Published var name = ""
@Published var age = 0
}
struct NameView: View {
@ObservedObject var user: UserModel
var body: some View {
Text(user.name) // 只用了 name,不过如果改user的age,也会触发不必要的重绘
}
}
// @Observable 改成了属性级追踪:SwiftUI 在求值 body 的过程中,记录下这次到底访问了哪些属性,然后只在这些属性变化时才让这个 view 失效。
// 上面的例子换成 @Observable 后,age 变化对 NameView 完全没有影响。
- 必须手动标 @Published(忘记了就是bug),而且嵌套对象不会自动传播
- 「@StateObject」、「@State」、「@ObservedObject」这些property wrapper 语义微妙,容易用错
而最新的2026年WWDC,没有关于Observation Framework的大更新,唯一相关的是「@State」从原来的「property wrapper」变成「macro」
而同时期——甚至早于Swift,Flutter那边发生的事情:
- 2020.06,Riverpod 诞生:Rémi Rousselet 作为 Provider 的继任者推出,摆脱 BuildContext、获得编译期安全;并在2021年9月发布Riverpod 1.0
- 2022,Riverpod 2.0:引入 Notifier / AsyncNotifier 与代码生成,AsyncValue 把三态封装成一等类型。
- 2025,Riverpod 3.0:接口统一、旧 Provider 降级 legacy、== 判断重建;细粒度靠 ref.select 等实现——看来Riverpod也是经历过踩坑、出坑的过程。
小结:这一时期,目标是只重建真正依赖了变化数据的那一小块UI,减少样板代码。
附上Riverpod3个版本的变迁简介
// 1.0版本写法: 手写,没有代码生成
final randomNumberServiceProvider = Provider<RandomNumberService>(
(ref) => RandomNumberGrpcService(),
);
// 依赖靠 ref.watch 串起来
final randomNumberRepositoryProvider = Provider<RandomNumberRepository>(
(ref) => RandomNumberRepositoryImpl(
service: ref.watch(randomNumberServiceProvider),
),
);
// 用的时候
final result = ref.watch(multiplyViewModelProvider);
final viewModel = ref.read(multiplyViewModelProvider.notifier);
// 2.0版本写法: 自动生成代码
@riverpod
RandomNumberService randomNumberService(RandomNumberServiceRef ref) =>
RandomNumberGrpcService();
@riverpod
RandomNumberRepository randomNumberRepository(RandomNumberRepositoryRef ref) =>
RandomNumberRepositoryImpl(service: ref.watch(randomNumberServiceProvider));
// 用的时候,和1.0一样
// 3.0版本写法
// 「Ref ref」写法和1.0版本的一样(改回去了)
@riverpod
RandomNumberService randomNumberService(Ref ref) => RandomNumberGrpcService();
@riverpod
RandomNumberRepository randomNumberRepository(Ref ref) =>
RandomNumberRepositoryImpl(service: ref.watch(randomNumberServiceProvider));
一张图,看完这一路
iOS 与 Flutter 两条赛道,起点不同、节奏也不同,却不约而同地朝着同一个方向演进——从「命令式 / 手动观察」,到「生命周期感知 / 响应式」,再到「细粒度 / 声明式」。中间那几条虚线串起的,正是两边各自跨过同一道门槛的时刻。
flowchart LR
classDef ios fill:#E3F2FD,stroke:#1565C0,color:#0D2A4A;
classDef flutter fill:#E8F5E9,stroke:#2E7D32,color:#16401A;
classDef era fill:#FFF8E1,stroke:#C0392B,color:#7A2418,font-weight:bold;
E1["命令式 / 手动观察<br/>The Imperative Era"]:::era
E2["生命周期感知 / 响应式<br/>Reactive & Lifecycle-Aware"]:::era
E3["细粒度 / 声明式<br/>Fine-grained & Declarative"]:::era
E1 --> E2 --> E3
subgraph IOS ["iOS 轨道"]
direction LR
I1["2008<br/>命令式 UI<br/>手动 KVO / Notification"]:::ios
I2["2012<br/>ReactiveCocoa<br/>FRP 函数式响应式"]:::ios
I3["2015<br/>RxSwift"]:::ios
I4["2019<br/>SwiftUI + Combine<br/>ObservableObject / @Published"]:::ios
I5["2023<br/>Observation<br/>@Observable 属性级细粒度追踪"]:::ios
I6["2026<br/>@State 由 property wrapper<br/>升级为 macro"]:::ios
I1 --> I2 --> I3 --> I4 --> I5 --> I6
end
subgraph FLT ["Flutter 轨道"]
direction LR
F1["2018<br/>BLoC 模式<br/>基于 Stream 单向数据流"]:::flutter
F2["2018 末<br/>Flutter 1.0 内置原语<br/>setState / InheritedWidget / ChangeNotifier"]:::flutter
F3["2019<br/>Provider<br/>官方推荐"]:::flutter
F4["2020<br/>Riverpod 诞生<br/>摆脱 BuildContext / 编译期安全"]:::flutter
F5["2021.09<br/>Riverpod 1.0"]:::flutter
F6["2022<br/>Riverpod 2.0<br/>Notifier / codegen / AsyncValue"]:::flutter
F7["2025<br/>Riverpod 3.0<br/>接口统一 / ref.select 细粒度"]:::flutter
F1 --> F2 --> F3 --> F4 --> F5 --> F6 --> F7
end
E1 -.-> I1
E1 -.-> F1
E2 -.-> I4
E2 -.-> F3
E3 -.-> I5
E3 -.-> F7