📱 系列二:MVVM 深度实战与项目重构 | 第5篇
ViewModel 核心原理与实战避坑:它是如何"死而复生"的?
本文导读
在上一篇文章中,我们搭建了 MVVM 的骨架。其中最关键的角色就是 ViewModel 。
你可能听过一句话:"ViewModel 可以在屏幕旋转时存活。"
但你是否想过:它是怎么活下来的? 为什么 Activity 都重建了,它还在?它是存在哪里的?会不会泄漏?
今天,我们将深入 ViewModel 的源码级原理 ,彻底搞懂 ViewModelStore、ViewModelProvider、HolderFragment 的工作机制,并解决企业项目中 ViewModelScope 内存泄漏、页面销毁数据残留、多 Fragment 数据共享 等核心痛点。
本文含大量源码解析与流程图,建议收藏。
0. 引子:一个困扰无数 Android 开发者的诡异现象
先看一段代码:
kotlin
// LoginActivity.kt
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
Log.d("ViewModelTest", "Activity onCreate: $this")
Log.d("ViewModelTest", "ViewModel hash: ${viewModel.hashCode()}")
}
}
操作:
- 打开
LoginActivity。 - 旋转屏幕(触发配置变更)。
- 观察 Logcat。
结果:
makefile
D/ViewModelTest: Activity onCreate: LoginActivity@a1b2c3
D/ViewModelTest: ViewModel hash: 123456
------------------ 旋转屏幕 ------------------
D/ViewModelTest: Activity onCreate: LoginActivity@d4e5f6 (Activity 变了!)
D/ViewModelTest: ViewModel hash: 123456 (ViewModel 没变!)
问题来了 :
Activity 明明被销毁重建了,为什么 ViewModel 还是原来那个?
它是被存到了哪里?是静态变量吗?会不会导致内存泄漏?
1. ViewModel 的"永生"之谜:源码级拆解
要搞清楚 ViewModel 为什么能活下来,我们必须剥开 ViewModelProvider 的外衣。
1.1 核心角色关系图
关键结论 :
ViewModel 不是 存在 Activity 里,也不是存在 Application 里。
它是存在一个 ViewModelStore 里,而这个 Store 又被一个 不会被销毁的 Fragment(HolderFragment) 拿着。
1.2 源码追踪:by viewModels() 做了什么?
当你写下:
kotlin
private val viewModel: LoginViewModel by viewModels()
实际上发生了以下调用链(简化版源码):
Step 1:获取 ViewModelStore
java
// ComponentActivity.java
public ViewModelStore getViewModelStore() {
if (mViewModelStore == null) {
// 关键:从 HolderFragment 获取
NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
Step 2:HolderFragment 的魔法
java
// HolderFragment.java
public class HolderFragment extends Fragment {
private ViewModelStore mViewModelStore = new ViewModelStore();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 关键行:设置为 Retain Instance
setRetainInstance(true);
}
}
这里就是"永生"的秘密 :
setRetainInstance(true) 告诉 FragmentManager:当 Activity 因为配置变更(如旋转)重建时,不要销毁这个 Fragment,直接把它"拿过来"给新的 Activity 用。
Step 3:ViewModelProvider 取实例
kotlin
// ViewModelProvider.kt
fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
var viewModel = store[key] // 先看 Store 里有没有
if (viewModel == null) {
viewModel = create(modelClass) // 没有就创建
store.put(key, viewModel) // 存入 Store
}
return viewModel
}
2. 实战避坑:ViewModel 的 5 个"夺命连环坑"
理解了原理,我们现在来解决企业项目中最常见的 5 个坑。
坑位一:ViewModel 持有 View 引用(必死无疑)
错误代码:
kotlin
class BadViewModel : ViewModel() {
private var textView: TextView? = null // ❌ 绝对禁止
fun bindView(view: TextView) {
this.textView = view
}
}
后果 :
Activity 旋转重建后,旧的 ViewModel 还拿着 旧的 TextView 引用 。
新的 Activity 里的 TextView 更新了,旧的 TextView 也收到了通知(虽然看不见),导致内存泄漏和数据错乱。
正解 :
ViewModel 只持有 LiveData/StateFlow。View 观察数据,ViewModel 永远不知道 View 的存在。
坑位二:静态持有 ViewModel(全局泄漏)
错误代码:
kotlin
// ❌ 灾难级错误
object GlobalHolder {
lateinit var viewModel: LoginViewModel
}
后果 :
ViewModel 的生命周期比 Activity 长得多,导致 Activity 永远无法被 GC 回收。
正解 :
ViewModel 必须由 ViewModelProvider 管理。如果真的需要全局共享,使用 Application Scope 的 ViewModel(极少情况)。
坑位三:ViewModelScope 未取消(协程泄漏)
这是最隐蔽的坑。很多人以为 ViewModel 销毁了,协程就自动取消了。
没错,ViewModel 销毁时 viewModelScope 会自动取消。
但是 ,如果你在 ViewModel 里启动了 普通的 CoroutineScope,那就完了。
错误代码:
kotlin
class LeakyViewModel : ViewModel() {
private val scope = CoroutineScope(Dispatchers.IO) // ❌ 错误:没有 Job
fun doWork() {
scope.launch {
while (true) {
delay(1000)
Log.d("Leak", "我还在跑...")
}
}
}
}
后果 :
Activity 销毁 -> ViewModel 销毁 -> 但是 scope 还在跑,因为没人取消它。
正解:
kotlin
// ✅ 方案 1:直接用 viewModelScope(推荐)
fun doWork() {
viewModelScope.launch {
// 自动取消
}
}
// ✅ 方案 2:自己管理 Job
class SafeViewModel : ViewModel() {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
override fun onCleared() {
super.onCleared()
job.cancel() // 手动取消
}
}
坑位四:多 Fragment 共享 ViewModel 的"脏数据"问题
场景 :
两个 Fragment 共享一个 Activity 级别的 ViewModel。
kotlin
// Fragment A
class FragmentA : Fragment() {
private val sharedVM: SharedViewModel by activityViewModels()
}
// Fragment B
class FragmentB : Fragment() {
private val sharedVM: SharedViewModel by activityViewModels()
}
坑点 :
Fragment A 设置了 sharedVM.userName = "Tom",然后 Fragment B 显示了。
下次 Fragment A 再次进入时,userName 还是 "Tom"(旧数据)。
正解 :使用 SingleLiveEvent 或 Channel 处理一次性事件,或者使用 SavedStateHandle 保存状态。
坑位五:页面销毁后数据残留(SavedStateHandle)
场景 :
用户在输入框填了半天,App 被系统杀死(不是旋转,是真杀),重启后数据没了。
解决方案 :SavedStateHandle
kotlin
@HiltViewModel
class LoginViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle // 注入
) : ViewModel() {
// 像 LiveData 一样使用,但它会自动保存
val account: MutableLiveData<String> = savedStateHandle.getLiveData("account")
fun saveAccount() {
account.value = "saved_account"
// 系统杀死 App 后,这个值会自动恢复
}
}
3. 企业级实战:ViewModel 生命周期规范
为了不出错,请严格执行以下 ViewModel 生命周期铁律:
3.1 初始化(Init)
kotlin
class LoginViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val loginUseCase: LoginUseCase
) : ViewModel() {
init {
// 1. 初始化 State(不要在这里做网络请求)
_uiState.value = LoginUiState()
// 2. 恢复保存的状态
restoreState()
}
private fun restoreState() {
val savedAccount = savedStateHandle.get<String>("account")
if (!savedAccount.isNullOrEmpty()) {
_uiState.update { it.copy(account = savedAccount) }
}
}
}
3.2 运行(Running)
kotlin
fun login() {
// 1. 使用 viewModelScope
viewModelScope.launch {
// 2. 更新 State
_uiState.update { it.copy(isLoading = true) }
// 3. 调用业务
val result = loginUseCase(...)
// 4. 保存状态(防止系统杀进程)
savedStateHandle["account"] = result.account
}
}
3.3 销毁(Clear)
kotlin
override fun onCleared() {
super.onCleared()
// 1. 取消所有未完成的协程(viewModelScope 会自动做,但保险起见)
// 2. 清理资源(如关闭 WebSocket、停止定时器等)
webSocket?.close()
timer?.cancel()
// 3. 清空回调(防止内存泄漏)
_uiState.value = null
}
4. 高级技巧:ViewModel 的作用域掌控
在企业项目中,ViewModel 的作用域有三种,用错就会出大问题。
4.1 单页面独立 ViewModel(最常用)
kotlin
// 每个 Activity/Fragment 都有自己的实例
private val vm: LoginViewModel by viewModels()
适用:登录页、详情页、表单页。
4.2 多 Fragment 共享 ViewModel(Activity 级)
kotlin
// 同一个 Activity 下的 Fragment 共享
private val vm: SharedViewModel by activityViewModels()
适用:主页面 + 多个 Tab Fragment、订单页 + 支付方式选择页。
4.3 全局共享 ViewModel(Application 级)
慎用! 除非真的是全局单例(如用户信息、配置信息)。
kotlin
// 在 Application 中持有
class MyApp : Application() {
val appViewModel: AppViewModel by lazy { AppViewModel() }
}
// 在 Activity 中取
val appVM = (application as MyApp).appViewModel
警告 :这种方式 绕过了 ViewModelStore 的生命周期管理 ,极易造成内存泄漏。推荐使用 Hilt 的 @Singleton 配合 ViewModel 来管理。
5. 总结:ViewModel 的"生存法则"
- ViewModel 活在 HolderFragment 里 ,因为
setRetainInstance(true)。 - ViewModel 不能持有 View,只能持有 State(LiveData/StateFlow)。
- 协程必须用
viewModelScope,别自己 new Scope。 - 页面销毁要清理资源 ,重写
onCleared()。 - 需要进程被杀恢复 ,用
SavedStateHandle。 - 共享要谨慎 ,Activity 级共享用
activityViewModels(),别乱用全局单例。
附录:ViewModel 故障排查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 屏幕旋转后数据没了 | 数据存在 Activity 字段里 | 移到 ViewModel |
| 屏幕旋转后数据重复请求 | 在 Activity onCreate 里无条件请求 | 加 Loading 状态判断 |
| 页面关闭后还在打印 Log | 协程未取消 | 用 viewModelScope |
| 内存一直涨 | ViewModel 持有 View/Context | 断开 View 引用 |
| 多 Fragment 数据错乱 | 共享 ViewModel 状态未重置 | 在 onDestroyView 重置 State |
下期预告 :
系列二:MVVM 深度实战与项目重构 | 第6篇:DataBinding & ViewBinding 实战落地
(我们将深入视图层,搞清楚为什么 Google 要把 findViewById 干掉,以及 DataBinding 的双刃剑该如何挥舞。)
如果这篇原理解析帮你扫清了迷雾,请分享给那些还在"凭感觉"用 ViewModel 的同事。