一、ViewModel 是否应该持有 Model?
答案:通常是持有引用,但方式要注意。
1️⃣ 理论
-
ViewModel 的职责:
- 将 Model 的数据转换成 View 可直接绑定的数据(格式化 / 排序 /过滤)
- 处理 View 的用户交互,通知 Model 或服务层
-
Model 的存在是 ViewModel 的前提:
- ViewModel 需要 Model 的状态才能生成 UI
- ViewModel 处理的通常是 Model 的快照或部分状态,而不是整份 Model
⚠️ 核心原则:持有是为了“只读/转换”,不要让 ViewModel 拥有 Model 的生命周期或复杂业务逻辑。
2️⃣ 持有方式
-
弱引用 / unowned
- 当 Model 的生命周期高于 ViewModel 时可用
- 避免循环引用
-
不可变拷贝(struct)
- 对于轻量级 Model(如 DTO、基本数据)
- ViewModel 持有值类型拷贝,不会影响 Model 的原始状态
- Swift 中 struct 的拷贝开销一般很小,适合中小型数据
-
只持有标识 / 数据流式订阅
- 对于大型 Model 或数据库对象(CoreData / Realm / Firebase)
- ViewModel 只持有 ID / 查询条件
- 通过绑定 / Combine / RxSwift 获取 Model 数据
- 避免一次性加载整个 Model,节省内存
二、ViewModel 很重、Model 很大时的问题
问题核心:ARC + 内存 + 数据更新频率
-
ARC 循环引用
- 如果 View 持有 ViewModel(通常是 strong)
- ViewModel 又 strong 持有 Model(Model 可能持有闭包 / Delegate 回 ViewModel)
- → 循环引用 → 内存泄漏
-
大 Model 拷贝成本
- struct/值拷贝每次都会复制内存
- 大数组 / 图像 / JSON 数据 → 内存占用高,性能下降
-
频繁更新导致 UI 卡顿
- ViewModel 持有大量数据,每次改变都触发
@Published或 KVO 通知 - 大量数据绑定 → 主线程压力大
- ViewModel 持有大量数据,每次改变都触发
三、优化方案(避免内存+性能问题)
1️⃣ 弱化或分离引用
class ViewModel {
weak var model: Model? // 避免循环引用
}
- 如果 Model 持有 VC / VM 的引用,必须用 weak/unowned
- 对 CoreData / Realm 对象,可用 ID 或查询
2️⃣ 用轻量快照替代整 Model
struct UserSnapshot {
let id: String
let name: String
let avatarURL: URL
}
class UserViewModel: ObservableObject {
@Published var user: UserSnapshot
init(user: User) {
self.user = UserSnapshot(id: user.id, name: user.name, avatarURL: user.avatarURL)
}
}
- ViewModel 持有快照而不是完整 Model
- 大数据量只传递必要字段
- 内存占用降低,ARC 压力小
3️⃣ 数据流式订阅
- 对于大型 Model,ViewModel 不直接持有,而是通过 Publisher / Combine / RxSwift 订阅数据
class UserViewModel: ObservableObject {
@Published var name: String = ""
private var cancellables = Set<AnyCancellable>()
init(userPublisher: AnyPublisher<User, Never>) {
userPublisher
.map { $0.name }
.assign(to: &$name)
}
}
-
好处:
- 不拷贝整个 Model
- 自动响应数据变化
- 内存占用稳定
4️⃣ 按需加载 & 分页
- 大数组 / 大列表不要一次性放进 ViewModel
- 使用分页或懒加载
- 与 SwiftUI / UITableView / UICollectionView 的 diffable 数据源结合,减少内存占用
5️⃣ 避免频繁对象创建
- ViewModel 中尽量复用对象,不要每次刷新数据就重建 ViewModel 或数组
- 对于不可变数据,使用
let避免 ARC 额外 retain/release 开销
✅ 总结策略
| 场景 | ViewModel 持有方式 | 优化方法 |
|---|---|---|
| 小型 Model / DTO | 值类型拷贝 | 直接持有 struct |
| 中型 Model / 对象 | 强引用 / 弱引用 | 弱引用 + Snapshot |
| 大型 Model / DB 对象 | 标识 + 流式订阅 | Combine / RxSwift + 懒加载 / 分页 |
| 循环引用风险 | 不可避免 | 使用 weak/unowned |
| UI 频繁刷新 | 任意大小 Model | 按需通知 / diffable 数据源 |
总结一句话:
ViewModel 可以持有 Model,但不要持有整个重量级对象;最好持有快照或标识,并结合流式订阅和懒加载来避免 ARC/内存/性能问题。