系列二: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 的同事。

相关推荐
执明wa27 分钟前
Android 开发中的设计模式入门:六大设计原则
android·设计模式
松仔log13 小时前
Java中级——组合和继承
android·java·开发语言
我命由我1234517 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
lilian23318 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_9557596218 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
余衫马18 小时前
2026最新版!Android Studio 极速安装与配置指南
android·ide·android studio
AFinalStone18 小时前
Android 7系统异常问题排查(五)Framework层(下)—System Server崩溃
android·tombstone·系统异常
AFinalStone18 小时前
Android 7系统异常问题排查(八)系统追踪—Trace机制与性能诊断
android·系统异常
极客猴子1 天前
Android录音转写频繁卡顿?多款APP长时间会议场景稳定性实测
android·人工智能·智能手机·飞书
Kapaseker1 天前
Boolean 变量到底该怎么命名?
android·kotlin