系列二:MVVM 深度实战与项目重构 | 第5篇 ViewModel 核心原理与实战避坑:它是如何“死而复生”的?

📱 系列二: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()}")
    }
}

操作

  1. 打开 LoginActivity
  2. 旋转屏幕(触发配置变更)。
  3. 观察 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 核心角色关系图
flowchart TB subgraph Activity ["Activity / Fragment"] A1["Activity 实例 A"] A2["Activity 实例 B (重建后)"] end subgraph ViewModelStoreOwner VMStore["ViewModelStore (持有 Map)"] end subgraph HolderFragment ["HolderFragment (非配置变更销毁)"] HF["Fragment (setRetainInstance=true)"] end subgraph ViewModel ["ViewModel 实例"] VM1["LoginViewModel"] end A1 -->|getViewModel| VMStore A2 -->|getViewModel| VMStore VMStore -->|contains| VM1 HF -->|holds| VMStore Activity -->|attaches to| HF

关键结论

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"(旧数据)。

正解 :使用 SingleLiveEventChannel 处理一次性事件,或者使用 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 的"生存法则"

  1. ViewModel 活在 HolderFragment 里 ,因为 setRetainInstance(true)
  2. ViewModel 不能持有 View,只能持有 State(LiveData/StateFlow)。
  3. 协程必须用 viewModelScope,别自己 new Scope。
  4. 页面销毁要清理资源 ,重写 onCleared()
  5. 需要进程被杀恢复 ,用 SavedStateHandle
  6. 共享要谨慎 ,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 的同事。

相关推荐
17715574311 小时前
unity6国际版安装及android SDK ,JDK,NDK安装
android
jingling5552 小时前
Flutter | 商城项目鸿蒙(OpenHarmony)适配实战
android·开发语言·前端·flutter·华为·harmonyos
黄林晴2 小时前
重磅:继SDK、NDK后谷歌新推出ADK!
android·kotlin
坏柠2 小时前
从一个设备控制面板开始,系统学习 LVGL 界面开发
android·javascript·学习
郑州光合科技余经理2 小时前
海外版外卖系统源码:支付/地图/多语言核心代码实现
android·java·前端·后端·架构·uni-app·php
核电机组2 小时前
flutter集成到Android混合开发
android·flutter
恋猫de小郭3 小时前
Android 17 内存管理将严格管控,App 要注意适配
android·前端·flutter
赏金术士3 小时前
Android 组件化学习项目(Kotlin + AGP8+)
android·学习·kotlin