ViewModel 底层原理深度解析:跨越屏幕旋转的"不老泉"与状态管理迷局
ViewModel 底层原理深度解析:跨越屏幕旋转的“不老泉”与状态管理迷局
ViewModel 是 Android Jetpack 中最核心、被应用最广泛的架构组件之一。所有的官方示例、第三方库(如 Compose、Navigation、Hilt)都与它深度绑定。很多开发者对 ViewModel 的认知仅停留在:“它是 MVVM 架构中存放数据的地方”、“屏幕旋转时它不会被销毁”。
但是,为什么它不会被销毁?它到底存在哪里?当系统内存不足杀掉进程时,它还能活下来吗?viewModelScope 是怎么实现自动取消协程的?
在这篇文章中,我们将彻底抛弃“面经”中浮于表面的概念,直接潜入 Android Framework 与 Jetpack 的源码深处,带你看看 ViewModel 真正的工作原理,以及在工业级实战中,它到底埋了哪些足以致命的深坑。
1. “不老泉”的秘密:为什么 ViewModel 能跨越配置变更?
在 Android 中,最让人头疼的就是配置变更(Configuration Change),比如屏幕旋转、系统语言切换、深色模式切换。一旦发生这些事件,当前的 Activity 会被直接销毁并重建。如果你的网络请求数据放在 Activity 里面,屏幕一转,数据全丢,又得重新请求。
ViewModel 的核心魔法就是:Activity 死了,它还活着;Activity 重建了,它还能自动挂载到新的 Activity 上。
1.1 ViewModelStore:数据的避风港
ViewModel 并不是孤立存在的,它被存放在一个叫做 ViewModelStore 的类中。你可以把 ViewModelStore 想象成一个保险箱:
public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();
final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}
final ViewModel get(String key) {
return mMap.get(key);
}
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear(); // 内部最终会调用 ViewModel.onCleared()
}
mMap.clear();
}
}
可以看到,ViewModelStore 本质上就是一个 HashMap,里面缓存了所有被创建出来的 ViewModel。所以,真正保证 ViewModel 存活的,是如何保证 ViewModelStore 在 Activity 销毁重建时不被销毁。
1.2 onRetainNonConfigurationInstance:框架层的走私通道
追踪 ComponentActivity 的源码,你会发现系统利用了 Android 早期就存在的一个机制:onRetainNonConfigurationInstance()。
当系统因为配置变更准备销毁 Activity 时,会调用这个方法。开发者可以在这里返回任意的 Object(对象)。这个对象会被 ActivityThread“走私”并暂存起来。当新的 Activity 被创建出来时,系统会把这个对象再通过 getLastNonConfigurationInstance() 还给你。
在 ComponentActivity 中,这个机制被巧妙地用来保存 ViewModelStore:
// ComponentActivity.java (简化版源码)
@Override
public final Object onRetainNonConfigurationInstance() {
// 1. 获取我们自己保存的配置对象(如果有的话)
Object custom = onRetainCustomNonConfigurationInstance();
// 2. 拿出当前的 ViewModelStore
ViewModelStore viewModelStore = mViewModelStore;
// 如果 viewModelStore 为空,但之前有过(比如没触发使用但重建了),尝试获取旧的
if (viewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
viewModelStore = nc.viewModelStore;
}
}
// 3. 把 ViewModelStore 打包进 NonConfigurationInstances
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.viewModelStore = viewModelStore; // 核心:保险箱被塞进去了!
return nci;
}
而在 Activity 初始化时(比如在构造函数或者 onCreate 之前),框架会把这个“走私”过来的保险箱拿出来:
// ComponentActivity.java 获取 ViewModelStore
public ViewModelStore getViewModelStore() {
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// 完美!我们拿到了上一个 Activity 留下的 ViewModelStore
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
【硬核总结】
ViewModel 之所以能活下来,根本原因是 Android 底层的 ActivityThread.performDestroyActivity 流程中,会调用 onRetainNonConfigurationInstance 获取状态,并暂存在 ActivityClientRecord 中。重建 Activity 时,再将该 Record 中的状态传递给新实例。这完全绕开了内存级别的对象回收。
2. 进程级别的死局与 SavedStateHandle 破局
屏幕旋转只是小 Case,我们考虑一个更极端的场景:进程死亡(Process Death)。
当用户把你的 App 切到后台,顺手开了一个吃内存的游戏(比如原神)。系统内存吃紧,你的 App 进程在后台被直接杀掉(Kill Process)。过了一会儿,用户通过多任务列表切回你的 App。
这时候,ViewModel 还在吗? 答案是:死的透透的。
因为整个进程都没了,内存里的 ViewModelStore 和 HashMap 灰飞烟灭。如果你的应用只是依赖 ViewModel 来保存草稿状态,用户切回 App 时会惊奇地发现,辛辛苦苦填了半个小时的表单,全丢了!
2.1 传统的 onSaveInstanceState 的困境
在没有 SavedStateHandle 之前,我们唯一的解法是在 Activity 的 onSaveInstanceState 里写 Bundle。但这就打破了 MVVM 的纯洁性:状态明明在 ViewModel 里,难道你要在 Activity 死前,把 ViewModel 里的状态掏出来,序列化存进 Bundle;等重建时,再从 Activity 把 Bundle 拿出来塞给 ViewModel?代码极其恶心。
2.2 SavedStateHandle 的源码级破局
Google 意识到了这个问题,推出了 SavedStateHandle。它的目标是:把 Bundle 的读写能力直接注入到 ViewModel 内部。
使用起来很简单:
class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
// 就像使用普通的 Map 一样
var searchQuery: String
get() = savedStateHandle.get("QUERY") ?: ""
set(value) { savedStateHandle.set("QUERY", value) }
// 甚至可以直接拿 Flow/LiveData
val queryFlow = savedStateHandle.getStateFlow("QUERY", "")
}
它是怎么做到的?
背后的机制叫做 SavedStateRegistry,它是一个横跨 Activity 和 ViewModel 的注册表系统。
- 注册阶段:当 ViewModel 被创建时,
SavedStateHandle会把自己作为一个SavedStateProvider注册到 Activity 的SavedStateRegistry中。 - 保存阶段:当系统调用 Activity 的
onSaveInstanceState(Bundle)时,Activity 的SavedStateRegistry会遍历所有的 Provider(包括我们 ViewModel 里的SavedStateHandle)。 - 序列化:
SavedStateHandle会把内部的数据结构转成一个Bundle,塞给 Activity 的大Bundle。最终这个大Bundle会被跨进程(Binder)传输到系统的ActivityManagerService (AMS)内存中(存放在ActivityRecord)。 - 恢复阶段:进程重启后,新的 Activity 在
onCreate(Bundle)中拿到了 AMS 返回的大Bundle,SavedStateRegistry进行反向分发。 - 重构 ViewModel:当 Factory 重新创建 ViewModel 时,它会从
SavedStateRegistry里抽出属于这个 ViewModel 的小Bundle,组装成SavedStateHandle,传递给 ViewModel 的构造函数。
这就是跨越进程生死的闭环。不过请注意:AMS 给每个 Activity 留下的 Bundle 空间是有限的(通常 1MB 以下,整个 Binder 缓冲区的限制),绝不允许在 SavedStateHandle 里面存大图、大列表数据!
3. ViewModel 实例是怎么被造出来的?(Provider & Factory)
理解了存储,再来看创建。为什么我们获取 ViewModel 的时候不能直接 new MyViewModel(),而要用委托 by viewModels() 或者 ViewModelProvider?
如果直接 new,那这个对象归你管,不受系统生命周期控制,屏幕一转它就成垃圾被 GC 了。必须要经过 ViewModelProvider 把对象登记到上面提到的 ViewModelStore 中。
3.1 ViewModelProvider 的双重检索逻辑
当你在代码里执行 ViewModelProvider(this).get(MyViewModel::class.java) 时,内部发生的事情其实是个经典的缓存加载逻辑:
// ViewModelProvider.java
public <T extends ViewModel> T get(String key, Class<T> modelClass) {
// 1. 去保险箱(ViewModelStore)里面找找有没有这把钥匙对应的 ViewModel
ViewModel viewModel = mViewModelStore.get(key);
// 2. 如果找到了,而且类型匹配,直接返回旧的!这就是屏幕旋转不丢状态的原因!
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}
// 3. 如果没找到(第一次创建,或者 Activity 被彻底销毁了),那就找兵工厂(Factory)造一个
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
} else {
viewModel = mFactory.create(modelClass);
}
// 4. 造完之后,放进保险箱
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
3.2 Kotlin 属性委托 by viewModels()
在 Kotlin 中我们普遍使用 val vm: MyViewModel by viewModels()。这个 by 关键字背后返回的是一个 ViewModelLazy 对象。
它利用了 Kotlin 的 Lazy 机制,只有在首次访问 vm 变量时,才会去触发上述的 Provider 获取逻辑。这也是为什么我们可以在 Activity 的全局变量位置直接声明它,而不用担心此时 Activity 还没有 onCreate(如果直接执行会崩溃,因为 ViewModelStore 还没准备好)。
4. viewModelScope:无感知的协程生命周期管理
现代 Android 开发离不开协程,而在 ViewModel 中发起网络请求的标准做法是使用 viewModelScope.launch { ... }。
最惊艳的设计在于:当 ViewModel 销毁时(onCleared),这些协程是怎么自动取消的?完全不需要开发者手动去调用 cancel()。
4.1 深入 viewModelScope 扩展属性
我们点进 viewModelScope 的源码,发现它竟然是一个 Kotlin 扩展属性(Extension Property):
// ViewModel.kt
public val ViewModel.viewModelScope: CoroutineScope
get() {
// 1. 尝试从 ViewModel 内部的一个 ConcurrentHashSet 拿缓存
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
// 2. 如果拿不到,创建一个新的 CloseableCoroutineScope
// 注意这里的 SupervisorJob 和 Dispatchers.Main.immediate
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
核心魔法在于这个 CloseableCoroutineScope 和 setTagIfAbsent。
ViewModel 内部除了维护核心数据,其实还维护了一个键值对的 Tag 集合:mBagOfTags。当我们访问 viewModelScope 时,它会创建一个 CloseableCoroutineScope 并塞进这个 Bag(包袱)里。
4.2 Closeable 接口的妙用
我们来看看 CloseableCoroutineScope 的实现:
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
// 核心在这里!实现了 java.io.Closeable 接口
override fun close() {
coroutineContext.cancel() // 取消协程作用域中的所有协程
}
}
伏笔回收!
还记得上面 ViewModelStore.clear() 里的代码吗?当 Activity 真死掉(比如用户按了返回键)时,系统会调用 ViewModel 的 clear():
// ViewModel.java
final void clear() {
mCleared = true;
// 遍历包袱里的所有 tag
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// 如果这个对象实现了 Closeable,就调用它的 close()!!!
closeWithRuntimeException(value);
}
}
}
onCleared(); // 留给开发者的回调
}
private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close(); // <--- 协程的 cancel() 被触发了!
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
极度优雅。Google 的工程师通过一个小小的 java.io.Closeable 接口,把协程的生命周期和 ViewModel 内部隐蔽的标签存储机制绑定在了一起,实现了近乎魔术般的自动取消机制。
5. 工业级开发:ViewModel 避坑与排雷指南
虽然 ViewModel 设计得很精妙,但在真实的业务代码中,往往因为开发者的误用而导致严重的 BUG。以下是绝对不能踩的深坑:
5.1 夺命深坑:ViewModel 导致的内存泄漏
原则:ViewModel 的生命周期可以比 Activity/Fragment 的视图寿命长得多。绝对、千万不能在 ViewModel 中持有对 View、Activity Context 或 Fragment 的引用。
反面教材:
class BadViewModel : ViewModel() {
var myTextView: TextView? = null // 致命错误 1
var context: Context? = null // 致命错误 2
fun passContext(ctx: Context) {
this.context = ctx // 如果传进来的是 Activity Context,恭喜,屏幕一转,LeakCanary 必报警
}
}
底层原因:
屏幕旋转时,旧的 Activity 会被系统销毁,理应被 GC。但是,由于 ViewModel 仍然存活在 ViewModelStore(被暂存在 ActivityThread 中),如果 ViewModel 的某个属性指向了旧的 Activity 或者它里面的 View,就会导致一整颗庞大的视图树无法被回收,造成巨大的内存泄漏。
正确解法:
- 绝对不传 View 或 Context。UI 的事情交回给 UI 层处理。
- 如果真需要 Context 拿资源(比如 getString),使用
AndroidViewModel并传入ApplicationContext。但即使是这样,业务开发中也应尽量将“获取资源”的操作放在 UI 层完成,ViewModel 只输出状态枚举或 ID。
5.2 作用域混乱:Navigation 下的多 Fragment 共享问题
ViewModel 的生命周期范围(Scope)取决于你找谁要这个 ViewModelProvider。
- 如果
ViewModelProvider(activity).get(),作用域就是整个 Activity。 - 如果
ViewModelProvider(fragment).get(),作用域就是这个 Fragment。
在使用 Navigation 组件时,有一个常见的需求:在一个子业务流程(比如注册向导 1/2/3 步对应 3 个 Fragment)中共享同一个 ViewModel。
坑点: 如果你用 Activity 级别的 ViewModel 来共享数据,会导致数据残留。用户完成了注册退回主页,再次点击注册进入向导时,由于 Activity 没销毁,Activity 级别的 ViewModel 也没销毁,上一次用户输入的密码还诡异地残留在输入框里。
破局点:使用 Navigation 图形级作用域(NavGraph Scope)
Navigation 框架为每一个后退栈中的 <navigation> 节点都提供了一个独立的 ViewModelStore。
// 在 Fragment 1, 2, 3 中,指定获取同一个 navGraph 节点下的 ViewModel
val sharedViewModel: RegisterViewModel by navGraphViewModels(R.id.register_graph)
底层原理是 Navigation 的 NavBackStackEntry 实现了 ViewModelStoreOwner 接口。当用户退出 register_graph 这个路由流时,Navigation 引擎会主动调用这个 Entry 的 ViewModelStore.clear(),精准地销毁这个只在这个业务流中生效的 ViewModel,保证没有脏数据残留。
5.3 协程在后台的“僵尸订阅”
我们在 UI 中收集 ViewModel 暴露的 StateFlow 时,经常会犯一个致命错误:直接在 lifecycleScope.launch 中 collect。
反面教材:
// MyFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch {
viewModel.locationFlow.collect { loc ->
// 更新地图定位
}
}
}
隐患分析:
lifecycleScope 只有在 Fragment onDestroy 时才会取消。当应用退到后台时,Fragment 并没有 Destroy(它只是 Stopped),这意味着上面的协程依然活着,依然在疯狂监听 Flow 并消费资源!如果这是一个高频的位置更新流,你的 App 即使在后台也会狂耗电。
必须使用的规范(生命周期感知型收集):
// 正确姿势:使用 repeatOnLifecycle
viewLifecycleOwner.lifecycleScope.launch {
// 只有在 Lifecycle 处于 STARTED 或更高状态时才执行,
// 当退到后台进入 STOPPED 时,内部协程会被自动取消!
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.locationFlow.collect { loc ->
// 安全的 UI 更新
}
}
}
或者使用更简化的 Kotlin 扩展:flowWithLifecycle。
结语
ViewModel 并不是一个简单的“数据类”,它是 Android 框架层、配置变更管理、进程状态挽救(SavedState)以及 Kotlin 协程生命周期管理的交叉枢纽。
- 理解
NonConfigurationInstances,让你知道为什么它不惧怕屏幕旋转。 - 理解
SavedStateRegistry,让你知道进程被强杀也能涅槃重生。 - 理解
Closeable的绑定,让你洞悉viewModelScope的精妙收尾机制。
写代码不仅要看 API 怎么调,更要通过源码,领略系统架构师在面对各种极端生命周期时的解题思路。当你掌握了这一切,你在 MVVM 架构上的每一次落键,都能精准无误、毫无破绽。