1. 为什么需要 ViewModel
1.1 传统开发中的四大痛点
| 痛点 | 表现 | 后果 |
|---|---|---|
| 数据丢失 | 屏幕旋转、语言切换时 Activity 重建,成员变量被清空 | 用户输入、滚动位置、未提交表单全部丢失 |
| 内存泄漏 | 异步任务持有 Activity 引用,Activity 已销毁但任务未结束 | 无法被 GC 回收,OOM 风险 |
| 职责混乱 | Activity 既管 UI 又管数据与业务 | 难以维护,违反单一职责 |
| 难以测试 | 业务逻辑与 Context、View 强耦合 | 只能做仪器化测试,成本高 |
1.2 ViewModel 的定位
一句话:ViewModel 是与界面生命周期解耦的 UI 状态容器,在配置变更时不会被销毁,只在 Owner(即提供 ViewModelStore 的宿主,通常是 Activity、Fragment 或 NavBackStackEntry)因非配置变更而被销毁时才会被清除。
对应 1.1 的痛点:ViewModel 直接解决配置变更导致的数据丢失,并有利于职责分离与单测;内存泄漏需正确使用(不持 Activity、用 viewModelScope 等)才能避免。
flowchart TB
subgraph 传统方式
A1[Activity] --> A2[成员变量]
A2 -->|旋转屏幕| A3[数据丢失]
end
subgraph ViewModel方式
B1[Activity] --> B2[ViewModel]
B2 --> B3[ViewModelStore]
B3 -->|旋转屏幕| B4[存到 Record<br/>新 Activity 取回]
B4 -->|新 Activity| B2
end
2. ViewModel 使用指南
2.1 基本用法:状态驱动 UI
核心思路:ViewModel 持有「UI 状态」,界面只做「观察 + 渲染 + 转发用户操作」。下面示例中的 viewModelScope 会在 ViewModel 被清除时自动取消其下协程,避免泄漏。
// ViewModel:状态 + 加载
class UserProfileViewModel(private val repo: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun load() { viewModelScope.launch { _uiState.update { it.copy(user = repo.getUser()) } } } // update 为 StateFlow 扩展,需 import kotlinx.coroutines.flow.update
}
// Activity/Fragment:观察 + 转发点击
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { /* 根据 it 更新 UI */ } }
}
refreshButton.setOnClickListener { viewModel.load() } // 仅转发,不写业务
2.2 四种创建方式对比
flowchart TB
subgraph 创建方式选择
Q[需要传参] --> A[否: by viewModels]
Q --> B[是: 需要注入]
B --> C[否: 自定义 Factory]
B --> D[是: Hilt ViewModel]
end
| 方式 | 写法 | 适用场景 |
|---|---|---|
| 基础 | ViewModelProvider(this).get(MyVM::class.java)(this 即 Owner,决定 ViewModel 归属哪个 Store、即作用域) | 理解原理、简单示例 |
| 委托(推荐) | private val vm: MyVM by viewModels() | 无参、单 Activity/Fragment |
| 自定义 Factory | by viewModels { MyFactory(args) } | 需要构造参数、无 Hilt |
| Hilt | @HiltViewModel + @Inject constructor() | 有 Hilt 的现代项目 |
在 Activity 中:通常只用 by viewModels(),作用域为当前 Activity,该 Activity 独享。
在 Fragment 中:有两种常见用法——
by viewModels()→ 作用域为当前 Fragment,该 Fragment 独享by activityViewModels()→ 作用域为宿主 Activity,多 Fragment 共享同一实例
2.3 Factory 的作用与为何要自定义
Factory 的作用:ViewModelProvider 从 Owner 的 ViewModelStore 里取 ViewModel 时,若没有缓存(第一次 get 这类 ViewModel),需要「创建」新实例。创建动作交给 ViewModelProvider.Factory:调用其 create(modelClass) 或 Lifecycle 2.5+ 的 create(modelClass, extras),由 Factory 决定如何 new 出这个 ViewModel 并返回。也就是说,Factory 负责「怎么构造 ViewModel」。
为什么要自定义:默认的 Factory 只能做无参构造,或通过框架提供的 CreationExtras(如 Application、SavedStateHandle)来构造。若你的 ViewModel 构造函数需要自己提供的参数(例如 userId、Repository、从 Intent 拿到的 id),默认 Factory 无法传入这些参数,就必须自定义 Factory:在创建 Factory 时带上这些参数,在 create() 里用它们调用 ViewModel 的带参构造函数。
用 Hilt 时,可由 Hilt 生成带依赖注入的 Factory,一般不需手写;只有「构造里需要运行时参数(如 id)」时才用 AssistedInject 或自定义 Factory 配合。
2.4 自定义 Factory 模板
class UserViewModel(private val userId: String, private val repo: UserRepository) : ViewModel()
class UserViewModelFactory(private val userId: String, private val repo: UserRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserViewModel(userId, repo) as T
}
// Lifecycle 2.5+ 的 create(modelClass, extras) 会多一个 extras 参数,可从 extras 取 Application、SavedStateHandle 等,实现无状态 Factory。
// 使用
private val viewModel: UserViewModel by viewModels { UserViewModelFactory("user123", repo) }
2.5 ViewModel 作用域体系(完整)
ViewModel 的生命周期与清除时机完全由「它所属的 ViewModelStoreOwner」决定:选哪个 API 就是选哪个 Owner,同一 Owner 内相同 key 只会有一个 ViewModel 实例,不同 Owner 之间互不共享。
2.5.1 谁可以实现 ViewModelStoreOwner?
| Owner 类型 | 常见代表 | ViewModel 何时被清除 |
|---|---|---|
| Activity | ComponentActivity、AppCompatActivity | Activity 被 finish 且非配置变更时 |
| Fragment | Fragment | 该 Fragment 被移除或其宿主 Activity 销毁时 |
| Navigation 回栈条目 | NavBackStackEntry | 该条目从 BackStack 中弹出时 |
因此:作用域 = 你选择哪个 Owner。同一 Owner 内相同 key 对应同一个 ViewModel 实例(key 默认为 "androidx.lifecycle.ViewModelProvider.DefaultKey:" + ViewModel 类全限定名)。下面所有 API 都是在「选 Owner」。
作用域与 Owner 的关系(同一 Owner ⇒ 同一 ViewModelStore ⇒ 同一 key 对应同一 ViewModel):
flowchart TB
subgraph Activity 作用域
A[Activity] --> AS[ViewModelStore A]
AS --> VM1[ViewModel 实例]
end
subgraph Fragment 作用域
F[Fragment] --> FS[ViewModelStore F]
FS --> VM2[ViewModel 实例]
end
subgraph 共享_同一 Activity
A2[Fragment1 / Fragment2] --> AS2[requireActivity 的 Store]
AS2 --> VM3[同一 ViewModel 实例]
end
2.5.2 作用域 API 一览
| 使用场景 | Kotlin API(View 体系) | 得到的 Owner | 典型用途 |
|---|---|---|---|
| Activity 内 | by viewModels() | 当前 Activity | 单屏数据、该 Activity 独享 |
| Fragment 内 | by viewModels() | 当前 Fragment | 该 Fragment 独享 |
| Fragment 需与 Activity 共享时 | by activityViewModels() | 宿主 Activity | 多 Fragment 共享同一 ViewModel |
| Fragment 需与父 Fragment 共享时 | by viewModels(ownerProducer = { requireParentFragment() }) | 父 Fragment | 嵌套 Fragment 共享 |
| 与导航图绑定 | by navGraphViewModels(R.id.xxx) 或 viewModels { getBackStackEntry(R.id.xxx) } | 该图在回栈中的 NavBackStackEntry | 同一导航图内共享,该 Entry 出栈后才清除 |
等价关系(便于理解):
// activityViewModels() 等价于
by viewModels(ownerProducer = { requireActivity() })
// navGraphViewModels(R.id.nav_graph) 等价于
by viewModels(ownerProducer = { findNavController().getBackStackEntry(R.id.nav_graph) })
注意:getBackStackEntry(R.id.nav_graph) 传入的是导航图的 id,返回的是该图在回栈中的那条 NavBackStackEntry。只要该 Entry 还在回栈中(用户仍在该图内导航),多个 Fragment 以同一 Entry 为 owner,获取的就是同一个 ViewModel;当该 Entry 被 pop 出栈时,其 ViewModelStore 会被清除。
2.5.3 多 Fragment 共享示例
SharedViewModel 内需提供选中的条目(如 val selectedItem: LiveData<Item>)和设置方法(如 fun selectItem(item: Item)),两个 Fragment 用同一 owner 拿到同一实例即可。
// 列表:写
class ListFragment : Fragment() {
private val shared: SharedViewModel by activityViewModels()
fun onItemClick(item: Item) { shared.selectItem(item) }
}
// 详情:读(同一实例)
class DetailFragment : Fragment() {
private val shared: SharedViewModel by activityViewModels()
override fun onViewCreated(...) { shared.selectedItem.observe(viewLifecycleOwner) { showDetail(it) } }
}
2.6 AndroidViewModel 使用场景
当 ViewModel 必须使用 Context 时(如访问 SharedPreferences、系统服务、Resources),应使用 AndroidViewModel,它在构造函数中接收 Application,从而持有的是应用级 Context,生命周期与进程一致,不会因 Activity 重建而泄漏。
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val prefs = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
// 或 application.resources、application.getSystemService(...) 等
}
- 优先使用普通 ViewModel:若可通过 Repository、依赖注入获取数据,不必用 AndroidViewModel。
- 不要在 ViewModel 中持有 Activity/Fragment/View 的引用;需要 Resources 或主题信息时,尽量通过接口或 UseCase 提供数据,而非在 ViewModel 中直接使用 Context。
3. ViewModel 实现原理
3.1 整体架构:谁持有谁
flowchart TB
subgraph 组件关系
Owner[ViewModelStoreOwner<br/>Activity/Fragment]
Store[ViewModelStore<br/>HashMap]
VM1[ViewModel 1]
VM2[ViewModel 2]
Owner --> Store
Store --> VM1
Store --> VM2
end
- ViewModelStoreOwner:接口,提供
getViewModelStore();Activity、Fragment、NavBackStackEntry 等实现了该接口。谁在什么时候调 getViewModelStore()?——ViewModelProvider.get() 内部需要先拿到 Store,就会调 owner.getViewModelStore(),再向 Store 要实例。 - ViewModelStore:内部用
HashMap<String, ViewModel>按 key 存储多个 ViewModel 实例。 - ViewModelProvider:根据 Owner 与 Factory,从 Owner 的 Store 中「获取已有实例或创建新实例」。
3.2 创建与获取流程
sequenceDiagram
participant UI as Activity/Fragment
participant VP as ViewModelProvider
participant Store as ViewModelStore
participant Factory as Factory
UI->>VP: get(MyViewModel::class)
VP->>VP: key = "DefaultKey:包名.MyViewModel"
VP->>Store: get(key)
alt 已存在
Store-->>VP: 已有实例
VP-->>UI: 返回该实例
else 不存在
VP->>Factory: create(MyViewModel::class)
Factory-->>VP: 新实例
VP->>Store: put(key, viewModel)
VP-->>UI: 返回新实例
end
要点:
- Key:默认是
"androidx.lifecycle.ViewModelProvider.DefaultKey:" + 类全限定名。 - 先查再建:同一 Owner 内相同 key 只会有一个实例。配置变更后新 Activity 首次 get 时,getViewModelStore() 会从 getLastNonConfigurationInstance().viewModelStore 取回旧 Store,再 store.get(key) 得到复用后的旧 ViewModel 实例。
- Factory:只有「没有缓存」时才调用,由 Factory.create() 负责如何 new 出 ViewModel(含带参构造)。
3.3 by viewModels() 做了什么
by viewModels() 是 Lazy 延迟初始化:首次访问 viewModel 属性时才执行 ViewModelProvider(owner).get(VM::class),结果缓存在 Lazy 中,之后始终返回同一实例。
// 等价关系(首次访问时执行,未必在 onCreate)
private val viewModel: MyViewModel by viewModels()
// 等价于:首次访问时 ViewModelProvider(this).get(MyViewModel::class.java)
activityViewModels() 与 viewModels() 的唯一区别是 Owner:前者以 requireActivity() 为 owner,因此多个 Fragment 获取的是同一 Activity 的 ViewModelStore 中的同一 ViewModel 实例。
3.4 屏幕旋转后 ViewModel 为何还在?(核心结论)
结论:ViewModel 不会随屏幕旋转丢失,是因为 ViewModelStore 在旧 Activity 销毁前被装进 NonConfigurationInstances,经 ActivityClientRecord 传给新 Activity,新 Activity 首次要 ViewModel 时从该对象里取回同一个 Store。全程内存引用传递,无序列化、无写盘。
NonConfigurationInstances:配置变更时用于「保留对象」的容器,由系统在销毁前向 Activity 索取。为何强调 ComponentActivity?——Activity 基类的 onRetainNonConfigurationInstance() 默认返回 null,不会保留任何对象;真正把 mViewModelStore 装进 NonConfigurationInstances 并返回的是 ComponentActivity(AppCompatActivity 的父类),所以 ViewModel 的保留逻辑在 ComponentActivity 里。系统把返回值存到 ActivityClientRecord.lastNonConfigurationInstances。
三阶段总览
总流程图:
flowchart LR
subgraph 一_保存
A1[旧 Activity] --> A2[onRetain 返回 NCI] --> A3[存入 Record]
end
subgraph 二_传递
B1[新 Activity] --> B2[attach 获得 NCI]
end
subgraph 三_取回
C1[getViewModelStore] --> C2[取回 Store] --> C3[get 同一实例]
end
A3 --> B1
B2 --> C1
三阶段简表:
| 阶段 | 时机 | 关键方法 / 字段 | 结果 |
|---|---|---|---|
| 一、保存 | 旧 Activity 销毁前 | retainNonConfigurationInstances() → onRetainNonConfigurationInstance() 返回 NonConfigurationInstances → 存入 ActivityClientRecord.lastNonConfigurationInstances | Store 引用从旧 Activity 到 Record |
| 二、传递 | 新 Activity attach 时 | activity.attach(..., record.lastNonConfigurationInstances) → Activity 赋给 mLastNonConfigurationInstances | 新 Activity 持有该引用,mViewModelStore 仍为 null |
| 三、取回 | 首次要 ViewModel 时 | getViewModelStore() 内从 getLastNonConfigurationInstance().viewModelStore 取回 → 赋给 mViewModelStore;无则 new ViewModelStore | 同一 Store → get(key) 得同一 ViewModel 实例 |
阶段一:保存
目的:把 ViewModelStore 的引用从旧 Activity 挪到 ActivityClientRecord,旧 Activity 可被回收,Record 稍后交给新 Activity。
步骤:
- ActivityThread 销毁旧 Activity 前,调旧 Activity 的 retainNonConfigurationInstances()。
- Activity 在 onRetainNonConfigurationInstance() 里 new NonConfigurationInstances,把 mViewModelStore 赋给其 viewModelStore,return(由 ComponentActivity 实现)。
- ActivityThread 将返回值存到 ActivityClientRecord.lastNonConfigurationInstances。
- 引用链变为 Record → NonConfigurationInstances → ViewModelStore,旧 Activity 可回收。
sequenceDiagram
participant AT as ActivityThread
participant Old as 旧 Activity
participant CA as ComponentActivity
participant Record as ActivityClientRecord
AT->>Old: retainNonConfigurationInstances()
Old->>CA: onRetainNonConfigurationInstance()
CA->>CA: nci.viewModelStore = mViewModelStore
CA-->>Old: return NonConfigurationInstances
Old-->>AT: return
AT->>Record: record.lastNonConfigurationInstances = 该对象
阶段二:传递
目的:把 Record 里存的 NonConfigurationInstances 交给新 Activity。
步骤:
- ActivityThread 用同一条 ActivityClientRecord 创建新 Activity,调 attach(..., record.lastNonConfigurationInstances, ...)。
- Activity 在 attach() 里把该参数赋给 mLastNonConfigurationInstances。
- 新 Activity 已持有含 viewModelStore 的 NonConfigurationInstances,此时尚未有人调 getViewModelStore(),mViewModelStore 仍为 null。
sequenceDiagram
participant AT as ActivityThread
participant Record as ActivityClientRecord
participant New as 新 Activity
AT->>New: 创建并 attach(..., record.lastNonConfigurationInstances)
New->>New: mLastNonConfigurationInstances = 参数
阶段三:取回
目的:新 Activity 首次要 ViewModel 时,从 mLastNonConfigurationInstances 里取回旧 ViewModelStore,ViewModelProvider.get 得到同一 ViewModel 实例(懒加载,非 onCreate 自动执行)。
步骤:
- 代码执行 ViewModelProvider(owner).get(MyViewModel::class),内部先调 owner.getViewModelStore()。
- Activity 的 getViewModelStore():若 mViewModelStore 不为 null 直接返回;为 null 则从 getLastNonConfigurationInstance().viewModelStore 取,有则赋给 mViewModelStore 并返回,无则 new ViewModelStore()(由 ComponentActivity 实现)。
- 配置变更场景下会取到旧 Store,赋给 mViewModelStore;ViewModelProvider 再 store.get(key),得到同一 ViewModel 实例,恢复完成。
sequenceDiagram
participant UI as Activity/Fragment
participant VP as ViewModelProvider
participant CA as ComponentActivity
participant Store as ViewModelStore
UI->>VP: get(MyViewModel::class)
VP->>CA: getViewModelStore()
alt mViewModelStore == null
CA->>CA: mViewModelStore = getLastNonConfigurationInstance().viewModelStore
end
CA-->>VP: return mViewModelStore
VP->>Store: get(key)
Store-->>VP: 同一 ViewModel 实例
VP-->>UI: 返回
三阶段一句话:
① 保存 → ActivityThread 调 retainNonConfigurationInstances,Activity 返回含 Store 的 NonConfigurationInstances(ComponentActivity 实现),存到 Record.lastNonConfigurationInstances。
② 传递 → attach 时把 record.lastNonConfigurationInstances 传入,Activity 赋给 mLastNonConfigurationInstances。
③ 取回 → 首次 getViewModelStore() 时从 getLastNonConfigurationInstance().viewModelStore 取回赋给 mViewModelStore,ViewModelProvider.get(key) 得同一实例。
数据流:同一 ViewModelStore 引用沿「retainNonConfigurationInstances → ActivityClientRecord → attach → mLastNonConfigurationInstances → getViewModelStore() → mViewModelStore」传递,无序列化、无写盘。
3.5 ViewModel 何时被清除
Activity 在 onDestroy() 时有一个标志位 isChangingConfigurations:若为 true 表示本次销毁是因为配置变更(旋转、语言等),系统不会调用 ViewModelStore.clear();若为 false 表示用户真正退出(返回键、finish),才会 clear 并触发 onCleared()。
flowchart TB
A[Activity.onDestroy] --> B[是否配置变更]
B -->|是| C[不调用 clear<br/>ViewModel 保留]
B -->|否| D[ViewModelStore.clear]
D --> E[逐个 ViewModel.onCleared]
E --> F[HashMap 清空]
| 场景 | ViewModel 是否保留 | 是否调用 onCleared() |
|---|---|---|
| 配置变更(旋转、语言等) | 保留并交给新 Activity | 否 |
| 正常退出(返回键、finish) | 不保留 | 是 |
| 进程被系统杀死 | 随进程消失 | 否 |
在 onCleared() 中应完成的清理:协程会随 viewModelScope 的 cancel 自动取消;此外应主动取消 Rx 的 Disposable、停止定时器、移除监听器等,避免泄漏和无效回调。
3.6 viewModelScope 原理简述
viewModelScope 是 ViewModel 的一个扩展属性,类型为 CoroutineScope。其行为可以概括为:
- 绑定时机:ViewModel 被创建时,会有一个与该 ViewModel 绑定的 CoroutineScope。
- 取消时机:当
ViewModelStore.clear()被调用、该 ViewModel 被清理时,会先触发onCleared(),同时与该 ViewModel 绑定的 Scope 会被 cancel,其下所有协程随之取消。 - 实现方式:内部通过
ViewModel的 Closeable 等机制,在 ViewModel 进入 cleared 状态时取消 Scope,避免 ViewModel 已失效后协程仍执行而造成泄漏或访问已销毁的 UI。
与清除的先后关系:
flowchart TB
A[Activity.onDestroy<br/>非配置变更] --> B[ViewModelStore.clear]
B --> C[每个 ViewModel.onCleared]
C --> D[viewModelScope.cancel]
D --> E[该 Scope 下协程全部取消]
因此:在 ViewModel 中启动协程时,务必使用 viewModelScope.launch { ... },不要使用 GlobalScope 或未与 ViewModel 绑定的 Scope,这样在配置变更时不会泄漏,在 Activity/Fragment 真正退出时协程会自动取消。
3.7 ViewModelProvider 的 key 与 Factory 进阶
key 的生成规则
ViewModelProvider.get(MyViewModel::class) 内部会生成一个 key,默认是:
"androidx.lifecycle.ViewModelProvider.DefaultKey:" + modelClass.canonicalName
因此同一 Owner 内,同一 ViewModel 类对应同一个默认 key,从而对应同一个 ViewModel 实例。若在同一 Owner 下需要同一类的多个实例,可使用带 key 的重载:
ViewModelProvider(owner).get("customKey", MyViewModel::class.java)
此时 key 为 "customKey",与默认 key 不同,会得到另一个实例。
CreationExtras 与无状态 Factory(Lifecycle 2.5+)
从 Lifecycle 2.5.0 起,ViewModelProvider.Factory 的 create 方法签名为:
fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T
CreationExtras 是一个只读的键值容器,在创建 ViewModel 时由框架传入,常用预定义 Key 包括:
| Key | 含义 |
|---|---|
AndroidViewModelFactory.APPLICATION_KEY | Application 实例 |
NewInstanceFactory.VIEW_MODEL_KEY | 你传给 get(key, modelClass) 的 key(String) |
SavedStateHandleSupport.DEFAULT_ARGS_KEY | 用于构造 SavedStateHandle 的 Bundle |
SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY | SavedStateRegistryOwner |
SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEY | ViewModelStoreOwner |
这样 Factory 可以无状态:不必在构造 Factory 时传入 Application、Owner 等,而是在 create(modelClass, extras) 中从 extras 读取。例如用 APPLICATION_KEY 构造需 Application 的 ViewModel,或从 DEFAULT_ARGS_KEY 取 Bundle 构造 SavedStateHandle。一个 Factory 可根据 modelClass 与 extras 支持多种 ViewModel 类型。
4. 内存泄漏与防护
4.1 什么是泄漏?为什么 ViewModel 容易牵涉进来?
泄漏:本应被回收的对象,因为还被别的存活对象引用,GC 收不走,占着内存不释放。
ViewModel 的设计是「配置变更时 Activity 销毁重建,ViewModel 不销毁」——ViewModel 比「当前这份 Activity 实例」活得更久。所以:
- 若 ViewModel(或它内部的协程、观察者)还引用着已销毁的 Activity/View,Activity 就永远无法被 GC → 泄漏。
- 简单记:生命周期比 Activity 短的对象(如随 Activity 销毁而释放的回调)可以持 Activity;生命周期比 Activity 长的(ViewModel、静态变量、未取消的协程)不能持 Activity,否则 Activity 无法被回收。
4.2 三类泄漏来源
下面三类问题的共同前提是:ViewModel(或协程、观察者)比当前 Activity 实例活得久——配置变更时系统不会调用 ViewModelStore.clear(),ViewModel 会经 ActivityClientRecord 传给新 Activity 复用;只有用户真正退出(非配置变更的 onDestroy)才会 clear。所以一旦 ViewModel 等还引用着已销毁的 Activity,就会形成泄漏。
flowchart TB
subgraph 泄漏来源
A[持有 Activity 或 View 引用]
B[未取消的协程或 Rx 订阅]
C[观察者未随 Lifecycle 释放]
end
A --> D[ViewModel 比 Activity 存活更久]
B --> D
C --> D
| 错误做法 | 引用链 | 结果 |
|---|---|---|
| ViewModel 持有 Activity / Fragment / View | ViewModelStore → ViewModel → Activity(已 onDestroy) | Activity 被 ViewModel 拽着,GC 收不走 → Activity 泄漏 |
| 用 GlobalScope 或未取消的协程/Rx | 协程/订阅仍在跑 → 闭包引用 ViewModel 或 Activity | ViewModel/Activity 本应回收,但被协程/订阅拽着 → 泄漏 |
| 观察者未移除,或 observer 强引用 Activity | ViewModel → LiveData → observer → Activity | Activity 已销毁,但 observer 仍在列表里 → Activity 泄漏 |
4.3 正确写法与原因
| 场景 | 正确做法 | 错误做法与原因 |
|---|---|---|
| Context | 只用 Application:AndroidViewModel(application) | 不要把 Activity/Fragment 通过构造或任何方式传进 ViewModel → 会强引用,配置变更时旧实例无法被 GC |
| 协程 | 只用 viewModelScope.launch { } | GlobalScope 绑定进程,ViewModel/Activity 销毁时不会被 cancel;协程一直跑且闭包可能引用 ViewModel/Activity → 泄漏。viewModelScope 会在 ViewModel clear 时 cancel,协程与引用一起释放 |
| RxJava | CompositeDisposable,在 onCleared() 里 composite.clear() | 不清理则订阅一直存在,可能拽着 ViewModel/Activity → 泄漏 |
| LiveData | observe(lifecycleOwner, observer) | 系统在 DESTROYED 时自动移除观察者。避免在 observer 里强引用 Activity/View,否则间接形成 ViewModel → observer → Activity 链 → 泄漏 |
4.4 防护清单(速查)
- 不持有 Activity / Fragment / View 引用;要 Context 只用 Application(如
AndroidViewModel) - 协程统一用 viewModelScope;Rx 用 CompositeDisposable 并在 onCleared() 中清理
- LiveData 用带 Lifecycle 的 observe(owner, ...);在 onCleared() 中释放定时器、监听器等
- 不把 ViewModel 存到静态变量或单例里
4.5 反模式与常见错误
| 不要 | 要 |
|---|---|
| 在 ViewModel 中持有 Activity/Fragment/View 或非 Application 的 Context | 只用 Application Context(如 AndroidViewModel),或通过接口/UseCase 提供数据 |
| 把对话框显隐、EditText 焦点等纯 UI 状态都塞进 ViewModel | ViewModel 只保留屏幕级业务状态;纯 UI 状态用 remember/View 管理 |
| 在 ViewModel 里写满网络、数据库、复杂业务逻辑 | 委托给 Repository/UseCase;ViewModel 只做「调依赖 + 映射 UI 状态 + 暴露流」 |
| 用 ViewModel 存大量数据或把 ViewModel 存静态/单例做全局共享 | 持久化用 Room/DataStore;跨屏共享用 activityViewModels() / navGraphViewModels() |
| 用 GlobalScope 或未在 onCleared() 中取消的 Rx/定时器/监听器 | 协程用 viewModelScope;Rx 用 CompositeDisposable 并在 onCleared() 中 clear() |
5. ViewModel 与 onSaveInstanceState
5.1 职责划分
| 维度 | ViewModel | onSaveInstanceState |
|---|---|---|
| 应对的问题 | 配置变更(旋转、语言等) | 进程被杀死后恢复 |
| 数据存在哪 | 内存,对象引用 | Bundle,可被系统持久化到磁盘 |
| 数据量与类型 | 受内存限制,任意对象 | 不宜过大(约 <1MB),基本类型/Parcelable/Serializable |
| 恢复时机 | 首次 getViewModelStore() 时取回旧 Store,再 get(key) 得同一 ViewModel 实例 | 需在 onCreate(savedInstanceState) 中手动读取 |
flowchart TB
subgraph ViewModel
V1[配置变更] --> V2[Store 引用传递]
V2 --> V3[无序列化]
end
subgraph Bundle
S1[进程被杀] --> S2[序列化到磁盘]
S2 --> S3[重建时反序列化]
end
5.2 现代做法:ViewModel + SavedStateHandle
既要「配置变更不丢」又要「进程死后能恢复」时,用 SavedStateHandle 存少量 key(如 id、搜索词),大块数据仍放 ViewModel。用法:在 ViewModel 构造参数中声明 SavedStateHandle,用 get/set、getStateFlow 读写;支持类型与 Bundle 一致,不宜过大;用户从最近任务划掉、强制停止、重启或 finish 后冷启动则无法恢复。
class UserProfileViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
var userId: String
get() = savedStateHandle.get<String>("user_id") ?: ""
set(v) { savedStateHandle.set("user_id", v) }
private val _user = MutableStateFlow<User?>(null)
val user = _user.asStateFlow()
fun loadUser() { viewModelScope.launch { _user.value = repo.getUser(userId) } }
}
5.3 选择策略
(此处「恢复」指进程被系统杀死后重新启动时能否获取上次的状态,非指长期持久化到磁盘。)
flowchart TB
Q[是否需要进程死亡后恢复] --> VM[否则仅 ViewModel]
Q --> Q2[数据量小且可序列化]
Q2 --> Bundle[是: SavedStateHandle 等]
Q2 --> SS[否: ViewModel 加 SavedStateHandle 存 key]
「ViewModel + SavedStateHandle 存 key」指:在 SavedStateHandle 里只存 id、搜索词等少量 key,进程死后用 key 恢复;大块数据仍放 ViewModel 或按 key 从 Repository 拉取。
6. SavedStateHandle 详解
SavedStateHandle 的用法与限制如下。ViewModel 只解决配置变更下的数据保留;进程被系统杀死后,ViewModel 会随进程消失。若要在进程重建后恢复少量 UI 状态(如用户 ID、搜索关键字、列表滚动位置 key),需使用 SavedStateHandle。
6.1 是什么、存在哪
- SavedStateHandle:一个键值对容器,由系统在 Activity/Fragment 的 SavedStateRegistry 背后支持。
- 保存时机:在 onStop() 前后,SavedStateRegistry 会收集并写入需保存的状态到 Bundle(可能与任务栈一起被系统持久化)。
- 恢复时机:Activity/Fragment 因进程重建而重新创建时,系统先恢复 Bundle,再在创建 ViewModel 时将基于该 Bundle 构造的 SavedStateHandle 通过构造函数或 Factory 传入。使用默认的
by viewModels()时框架会自动注入 SavedStateHandle,在 ViewModel 构造参数中声明即可,无需手写 Factory。
因此:SavedStateHandle 里放的数据,既能在配置变更后存在(因为 ViewModel 本身还在),也能在进程死亡后被系统恢复(因为写进了 SavedStateRegistry 的 Bundle)。
SavedStateHandle 的写入与恢复流程(与 ViewModel 的「内存引用」恢复是两条独立路径):
flowchart TB
subgraph 写入_进程存活时
W1[ViewModel 内 set] --> W2[SavedStateRegistry 收集]
W2 --> W3[onStop 前后写入 Bundle]
W3 --> W4[系统可能序列化到磁盘]
end
subgraph 恢复_进程死后重建
R1[Activity 重建] --> R2[系统恢复 Bundle]
R2 --> R3[构造 SavedStateHandle]
R3 --> R4[get 获取进程死前的值]
end
W4 -->|进程被杀| R1
6.2 基本用法
在 ViewModel 构造参数中声明 SavedStateHandle 即可,默认 by viewModels() 会自动注入。下面示例为 get/set 与 getStateFlow 的用法:
class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
var searchQuery: String
get() = savedStateHandle.get<String>("query") ?: ""
set(v) { savedStateHandle.set("query", v) }
val queryFlow = savedStateHandle.getStateFlow("query", "") // 可观察
}
- get/set:读写;getStateFlow(key, default):暴露为 Flow 便于 UI 收集。
6.3 支持的类型与限制
可保存的类型与 Bundle 一致:基本类型、String、Parcelable、Serializable、以及受支持的集合等。不宜存大对象或大量数据,总大小建议控制在约 1MB 以内,否则可能影响保存/恢复性能或被系统限制。
6.4 何时能恢复、何时无法恢复
能恢复的典型情况:进程仅因内存紧张被系统回收,用户从最近任务再次进入应用;或从其他应用返回时进程被重建。只要任务栈与 SavedState 被系统保留,就会恢复。
无法恢复的情况:下次启动为「冷启动」,系统不会提供上次的 SavedState 数据,例如:
- 用户从最近任务列表划掉应用
- 用户在设置中强制停止应用
- 设备重启
- 用户按返回键或调用
finish()退出该 Activity 后,再重新启动应用
7. ViewModel 与 Compose / Navigation
7.1 Compose 中的 viewModel()
在 Compose 中,ViewModel 的作用域不能绑定到「某个 @Composable 函数」——因为 Composable 会随重组多次执行,若每次拿新的 ViewModel 会丢状态且难以管理。必须绑定到 ViewModelStoreOwner(如宿主 Activity、Fragment,或当前 Navigation 目的地的 NavBackStackEntry)。用法示例:
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// 根据 uiState 渲染
}
- viewModel() 使用的 owner 来自 LocalViewModelStoreOwner(在 NavHost 的 composable 中一般为当前目的地的 NavBackStackEntry,否则为宿主 Activity 或 Fragment)。
7.2 指定 Owner(Activity / 父 Fragment / 导航图)
需要与 Activity 或父 Fragment 共享 ViewModel 时,显式传入 viewModelStoreOwner:
// 与 Activity 共享:在 Fragment 的 Composable 中可用 requireActivity();纯 Compose 中可用 LocalContext.current 转成 Activity(确保 owner 为 Activity 时)
// 与某导航图共享: val entry = navController.getBackStackEntry("graphId"); viewModel(entry)
7.3 Fragment + Navigation:navGraphViewModels
在 View 体系下,若希望 ViewModel 与某个导航图绑定(在该图对应的 NavBackStackEntry 仍在回栈内时,多个 Fragment 共享同一实例;该 Entry 出栈后清除):
import androidx.navigation.fragment.navGraphViewModels
class MyFragment : Fragment() {
private val sharedViewModel: SharedViewModel by navGraphViewModels(R.id.nav_graph)
}
等价于 viewModels(ownerProducer = { findNavController().getBackStackEntry(R.id.nav_graph) })。使用 Hilt 时可用 hiltNavGraphViewModels(R.id.nav_graph),由 Hilt 生成可注入依赖的 ViewModelProvider.Factory。
8. ViewModel 与 Hilt
8.1 基本用法:@HiltViewModel 与 @Inject
使用 Hilt 时,ViewModel 由 Hilt 生成的 ViewModelProvider.Factory 创建,无需手写 Factory;构造参数(如 Repository、SavedStateHandle)可由 Hilt 注入:
@HiltViewModel
class UserViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repo: UserRepository
) : ViewModel() { fun loadUser() { viewModelScope.launch { /* 用 repo */ } } }
Activity/Fragment 加 @AndroidEntryPoint,仍用 by viewModels() 获取。
Hilt 会为 ViewModel 生成 ViewModelProvider.Factory,并在内部使用 ViewModelProvider(owner, factory).get(),因此作用域仍由 owner(Activity、Fragment 或 NavBackStackEntry)决定。
8.2 ViewModel 内依赖的作用域
- 注入到 ViewModel 的依赖(如 Repository)通常由 Singleton 或 Activity 等 Hilt 组件提供,其生命周期由 Hilt 作用域决定,与 ViewModel 何时被清除无必然关系。
- 若希望某依赖「仅在该 ViewModel 实例内有效」,可使用 Hilt 提供的 @ViewModelScoped;同一 ViewModel 类的不同实例会得到不同的 @ViewModelScoped 实例。
8.3 带运行时参数的 ViewModel(AssistedInject)
若 ViewModel 除 Hilt 可注入的类型外,还需要运行时参数(如从 Intent/Navigation 传来的 id),常见做法是:把 id 放进 SavedStateHandle(例如通过 Navigation 的 defaultArguments 或 Activity 的 intent.extras),ViewModel 构造里声明 SavedStateHandle 由 Hilt 注入,在构造函数内用 savedStateHandle.get<String>("id") 取 id,其余依赖照常由 Hilt 注入。若必须通过 Factory 传参,可用 @AssistedInject 与 AssistedFactory,详见 Hilt 官方文档「ViewModel with assisted state」。
9. ViewModel 状态暴露:LiveData / StateFlow / SharedFlow
9.1 三者简要对比
| 特性 | LiveData | StateFlow | SharedFlow |
|---|---|---|---|
| 生命周期感知 | 是,观察者在 Lifecycle DESTROYED 时自动移除,不会泄漏 | 否,需配合 repeatOnLifecycle 等 | 否 |
| 必有初始值 | 否 | 是(value) | 否 |
| 重放 | 仅最新值 | 仅最新值 | 可配置 replay |
| 场景 | 传统 View、DataBinding | Compose、现代 Kotlin 优先 | 一次性事件、多订阅者 |
9.2 在 ViewModel 中如何选
- Compose 或 Kotlin 优先:推荐用 StateFlow 暴露 UI 状态,在 UI 层用
collectAsStateWithLifecycle()或repeatOnLifecycle(Lifecycle.State.STARTED) { flow.collect { } }收集,避免在 STOPPED/DESTROYED 时仍收集造成浪费或问题。 - 传统 View + XML:LiveData 仍适用,
observe(lifecycleOwner, observer)即可,由系统在 DESTROYED 时移除观察者。 - 一次性事件(Toast、导航、Snackbar):用 SharedFlow(replay=0) 或 Channel 在 ViewModel 中发送,在 UI 层按需消费,避免配置变更后重复触发。
9.3 推荐模式:单一 StateFlow + 事件流
状态用 StateFlow(有初始值、始终有当前值);一次性事件(Toast、导航、Snackbar)用 SharedFlow(replay = 0),这样配置变更后不会重放旧事件,避免重复触发。
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()
private val _events = MutableSharedFlow<UiEvent>(replay = 0)
10. ViewModel 测试
10.1 为何好测
ViewModel 不依赖 Android 框架的 Context、View,只依赖 Repository、UseCase 等接口,便于在 JVM 单元测试中 mock 依赖、验证状态与副作用。
10.2 依赖与规则
- androidx.arch.core:core-testing:提供 InstantTaskExecutorRule,让 LiveData 的 postValue 等在同一线程同步执行,便于断言。
- kotlinx-coroutines-test:提供 TestScope、runTest、UnconfinedTestDispatcher 等,在测试中控制协程调度,避免真实 delay/Dispatcher.Main。若测试中会触发 Main 协程,需用 TestDispatcher 替换 Main(如
Dispatchers.setMain(UnconfinedTestDispatcher()))或使用提供 MainDispatcherRule 的 test 库。
@get:Rule val instantExecutorRule = InstantTaskExecutorRule() // LiveData 同步
// MainDispatcherRule 需自行实现或使用 test 库提供的 Main 替换,以便 viewModelScope 等在主线程的测试中可控
10.3 测试思路
- 用 MockK 或 Mockito 构造 FakeRepository / Mock,注入 ViewModel。
- 调用 ViewModel 的方法(如 loadUser、onRefresh)。
- 对 StateFlow:在 runTest 中 collect 或取 value,断言状态变化。
- 对 LiveData:用 InstantTaskExecutorRule 后直接取 value 或 observe 一次断言。
- 验证 viewModelScope 内协程:在 TestDispatcher 下 runTest,确认在 cancel 或 advanceUntilIdle 后无多余发射或回调。
11. 总结与面试要点
11.1 核心价值
- 配置变更不丢数据:ViewModelStore 经 retainNonConfigurationInstances → Record → attach → getViewModelStore() 传给新 Activity。
- 生命周期边界清晰:仅在 Owner 因非配置变更而被销毁时清除,便于在 onCleared() 中做资源释放。
- 便于架构分层:UI 只观察状态和转发事件,业务与数据在 ViewModel/Repository。
- 易测试:不依赖 Android 组件的 ViewModel 可单独做单元测试。
11.2 最佳实践简表
| 建议做 | 建议不做 |
|---|---|
用 by viewModels() / activityViewModels() | 在 ViewModel 中持有 View/Activity 引用 |
用 viewModelScope 启动协程 | 用 GlobalScope 或未管理的 Disposable |
| 用 Repository + 状态容器(StateFlow/LiveData) | 在 ViewModel 中堆砌大量裸数据 |
| 需要进程恢复时用 SavedStateHandle | 忽略 onCleared 的资源释放 |
| 为 ViewModel 写单元测试 | 把 ViewModel 当全局单例用 |
11.3 常见面试题速答
-
旋转屏幕后 ViewModel 为什么还在?
配置变更销毁前,ActivityThread 调retainNonConfigurationInstances(),Activity 在onRetainNonConfigurationInstance()中返回含 Store 的 NonConfigurationInstances(ComponentActivity 实现),ActivityThread 存入ActivityClientRecord.lastNonConfigurationInstances;新 Activity 在attach()时收到该对象赋给mLastNonConfigurationInstances,首次getViewModelStore()时从getLastNonConfigurationInstance().viewModelStore取回并赋给mViewModelStore,ViewModel 实例一直在内存里。 -
ViewModel 和 LiveData 的区别?
ViewModel 负责持有和管理 UI 相关状态;LiveData 是一种可观察、生命周期感知的容器,常用来把 ViewModel 里的状态暴露给 UI。两者配合使用,职责不同。 -
如何避免 ViewModel 导致的内存泄漏?
只用 Application Context;协程用 viewModelScope;Rx 在 onCleared() 中 dispose;不在 ViewModel 中持有 Activity/Fragment/View;LiveData 用带 Lifecycle 的 observe。 -
ViewModel 和 onSaveInstanceState 分别解决什么问题?
ViewModel 解决配置变更导致的数据丢失(内存引用传递);onSaveInstanceState 解决进程被杀死后的恢复(Bundle 序列化)。两者可结合 SavedStateHandle 一起用。 -
ViewModel 有哪些作用域?
Activity 内(viewModels())、Fragment 内(viewModels())、与 Activity 共享(activityViewModels())、与父 Fragment 共享(ownerProducer = { requireParentFragment() })、与导航图绑定(navGraphViewModels(R.id.xxx))。作用域由 ViewModelStoreOwner 决定,Owner 因非配置变更被销毁时 ViewModel 被清除。 -
SavedStateHandle 和 ViewModel 的区别?
ViewModel 解决配置变更时内存中数据保留;SavedStateHandle 在 ViewModel 内提供键值对,由 SavedStateRegistry 支持,可在进程死亡后随 Activity 恢复。适合存少量关键状态(如 id、搜索词),大块数据仍放 ViewModel 或 Repository。 -
Compose 里怎么用 ViewModel?
使用viewModel()获取作用域为「当前 Composition 的 ViewModelStoreOwner」的 ViewModel;在 Navigation 中一般为当前目的地。需共享时传viewModelStoreOwner参数(如 requireActivity()、parentBackStackEntry)。 -
如何对 ViewModel 做单元测试?
Mock Repository 等依赖并注入;使用 InstantTaskExecutorRule 让 LiveData 同步;使用 kotlinx-coroutines-test 的 runTest/TestDispatcher 控制协程;对 StateFlow 取 value 或 collect 断言,对 LiveData 观察一次或取 value 断言。