要搞懂 ViewModel 的 "不死之谜",我们先讲个《管家与主人的故事》,再扒代码、画时序图 ------ 保证像看动画片一样明白!
一、先听个故事:ViewModel 是 "不会下岗的管家"
假设你(用户)是 "房子主人",手机里的 APP 页面(比如记笔记的页面)是你的 "临时住所"(Activity),而 ViewModel 是你雇的 "管家",负责保管你的重要物品(比如没写完的笔记)。
剧情 1:第一次入住(Activity 首次创建)
你第一次住进 "临时住所"(打开 APP 页面),需要一个管家管笔记。你不会自己找管家,而是找 "居委会"(ViewModelStore)要 ------ 但居委会不管招人,只负责 "存档管家信息"。真正帮你找管家的是 "中介"(ViewModelProvider):
- 中介问居委会:"有负责'记笔记'的管家吗?"
- 居委会查了查档案(HashMap):"没有,新招一个吧!"
- 中介招了个新管家(ViewModel 实例),把他的信息存到居委会档案里,再把管家介绍给你。
- 你把没写完的笔记(数据)交给管家保管,放心用手机。
剧情 2:换衣服不换管家(屏幕旋转 = 换衣服)
你觉得房间朝向不好(触发屏幕旋转),于是换了件 "新衣服"(系统销毁旧 Activity,创建新 Activity)------ 但你还是你,不需要换管家!
这时候神奇的事发生了:
- 系统要拆旧房子(销毁旧 Activity)前,先把居委会的 "管家档案"(ViewModelStore)偷偷藏到 "社区仓库"(NonConfigurationInstance,简称 NCI)里 ------ 这是系统专门存 "不想丢的东西" 的地方。
- 旧房子拆了(旧 Activity 销毁),但管家还在居委会档案里,没下岗。
- 你住进新房子(新 Activity 创建),又找中介要管家。
- 中介再问居委会:"有'记笔记'的管家吗?"
- 居委会说:"有!社区仓库里存着旧档案,我去拿!" 于是从 NCI 里取出原来的 ViewModelStore,找到那个管家。
- 中介把老管家带给你,你一看:"哟,还是你!我的笔记呢?" 管家掏出笔记,一点没丢 ------ 这就是旋转屏幕数据不丢的原因!
剧情 3:搬家才辞管家(Activity 真正销毁)
你住够了,收拾东西搬家(点击返回键,Activity finish)------ 这时候才需要辞掉管家:
- 系统拆房子前,问你:"是换衣服(配置变更)还是真搬家(finish)?" 你说 "真搬家"(isChangingConfigurations ()=false)。
- 居委会收到 "真搬家" 通知,立刻清理档案:叫管家把笔记交接好(调用 ViewModel 的 onCleared ()),然后删掉档案(清空 HashMap)。
- 管家正式下岗(ViewModel 失去引用,等着被 GC 回收),房子拆了(Activity 销毁)。
二、用代码验证故事:看管家 "上岗 / 下岗" 日志
光说不练假把式,我们写 30 行代码,看日志就知道 ViewModel 没换!
1. 定义 "管家"(ViewModel 类)
管家负责存笔记,下岗时会喊一声(onCleared ()):
kotlin
import androidx.lifecycle.ViewModel
import android.util.Log
// 记笔记的管家
class NoteViewModel : ViewModel() {
// 管家保管的笔记(核心数据)
var noteContent: String = "初始空白笔记"
// 管家下岗前的收尾工作(比如关闭数据库连接、取消网络请求)
override fun onCleared() {
super.onCleared()
Log.d("ViewModel故事", "⚠️ 管家下岗!已清空临时数据")
}
}
2. 定义 "主人的住所"(Activity 类)
主人(Activity)在 "入住"(onCreate)时找中介要管家,搬家(finish)时触发管家下岗:
kotlin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import android.util.Log
import android.widget.Button
import android.widget.TextView
class MainActivity : AppCompatActivity() {
// 主人的管家(延迟初始化)
private lateinit var noteViewModel: NoteViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. 找中介(ViewModelProvider)要管家:传入this(当前住所=作用域)
noteViewModel = ViewModelProvider(this)[NoteViewModel::class.java]
// 2. 显示管家保管的笔记(证明数据没丢)
val tvNote = findViewById<TextView>(R.id.tv_note)
tvNote.text = "管家的笔记:${noteViewModel.noteContent}\n管家ID:${noteViewModel.hashCode()}"
Log.d("ViewModel故事", "🏠 新住所入住,管家ID:${noteViewModel.hashCode()}")
// 3. 模拟主人修改笔记(给管家新任务)
val btnEdit = findViewById<Button>(R.id.btn_edit)
btnEdit.setOnClickListener {
noteViewModel.noteContent = "今天学会了ViewModel!"
tvNote.text = "管家的笔记:${noteViewModel.noteContent}\n管家ID:${noteViewModel.hashCode()}"
}
}
// 4. 住所销毁时,判断是"换衣服"还是"真搬家"
override fun onDestroy() {
super.onDestroy()
val isChangeConfig = isChangingConfigurations()
Log.d("ViewModel故事", "🏠 住所销毁,是否换衣服(配置变更):$isChangeConfig")
}
}
3. 看日志!验证 "管家没换"
运行 APP 后,观察 Logcat 的日志,会看到两个关键现象:
现象 1:旋转屏幕,管家 ID 不变(没换新管家)
- 首次打开 APP:
🏠 新住所入住,管家ID:12345
(假设 ID 是 12345) - 点击 "编辑笔记":笔记变成 "今天学会了 ViewModel!"
- 旋转屏幕:
🏠 新住所入住,管家ID:12345
(ID 还是 12345!),笔记也没丢 - 旋转时销毁旧 Activity:
🏠 住所销毁,是否换衣服(配置变更):true
(换衣服,不辞退管家)
现象 2:按返回键(真搬家),管家下岗
- 按返回键:
🏠 住所销毁,是否换衣服(配置变更):false
(真搬家) - 紧接着:
⚠️ 管家下岗!已清空临时数据
(管家被辞退)
三、扒原理:为什么管家 "换衣服不下岗"?
故事里的 "居委会""中介""社区仓库",对应 Android 源码里的 3 个核心角色:
故事里的角色 | 源码里的类 / 概念 | 核心作用 |
---|---|---|
管家 | ViewModel | 存数据,有下岗收尾方法(onCleared ()) |
居委会 | ViewModelStore | 用 HashMap 存 ViewModel(key=ViewModel 类名,value = 实例) |
中介 | ViewModelProvider | 帮 Activity 找 ViewModel:先查 ViewModelStore,没有再新建 |
社区仓库 | NonConfigurationInstance(NCI) | 配置变更时,保存 ViewModelStore,跨 Activity 实例传递 |
关键 1:ViewModelStore------ 管家的 "档案库"
ViewModelStore 本质是个 "装管家的 HashMap",源码超级简单(简化后):
java
public class ViewModelStore {
// 用HashMap存ViewModel:key是ViewModel的类名(比如"NoteViewModel")
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.onCleared(); // 逐个喊管家下岗
}
mMap.clear(); // 清空档案
}
}
关键 2:NonConfigurationInstance(NCI)------ 跨 Activity 传档案库
为什么旋转屏幕后,新 Activity 能拿到旧的 ViewModelStore?秘密在NCI------ 系统专门用来保存 "配置变更时不想丢的数据" 的容器。
当屏幕旋转时,系统会做 3 件事:
- 销毁旧 Activity 前,把旧 Activity 的 ViewModelStore 装进 NCI;
- 系统暂时保存这个 NCI(不会随旧 Activity 销毁);
- 新 Activity 创建时,系统把旧 NCI 传给新 Activity,新 Activity 从 NCI 里取出 ViewModelStore。
这一步的核心是 Activity 的getViewModelStore()
方法(源码简化):
java
public ViewModelStore getViewModelStore() {
// 1. 先从系统拿旧Activity的NCI(里面有ViewModelStore)
NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null && nc.viewModelStore != null) {
// 2. 有旧的ViewModelStore,直接用(不用新建)
return nc.viewModelStore;
}
// 3. 没有旧的,新建ViewModelStore并装进新NCI
ViewModelStore viewModelStore = new ViewModelStore();
NonConfigurationInstances newNc = new NonConfigurationInstances();
newNc.viewModelStore = viewModelStore;
setLastNonConfigurationInstance(newNc);
return viewModelStore;
}
关键 3:什么时候辞退管家?看 "是否换衣服"
只有当 Activity "真搬家"(不是换衣服)时,才会辞退管家。判断依据是isChangingConfigurations()
:
- 屏幕旋转、语言切换等配置变更 :
isChangingConfigurations()=true
→ onDestroy () 时不调用 ViewModelStore.clear () → 管家留任; - 点击返回键(finish)、系统内存不足杀后台真销毁 :
isChangingConfigurations()=false
→ onDestroy () 时调用 ViewModelStore.clear () → 管家下岗。
这一步的逻辑在 Activity 的父类ComponentActivity
的 ComponentActivity () 里(简化代码,基于androidx.activity:1.8.0是通过lifecycle监听ON_DESTROY实现):
java
public ComponentActivity() {
Lifecycle lifecycle = getLifecycle();
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// And clear the ViewModelStore
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
}
四、时序图:一目了然看流程
用时序图把两个核心场景画出来.
场景 1:屏幕旋转(配置变更)→ 管家留任
text
用户 系统 旧Activity 新Activity ViewModelProvider ViewModelStore ViewModel
| | | | | | |
| 旋转屏幕 | | | | | |
|----------->| | | | | |
| | 准备销毁旧Activity | | | |
| |-------------------------->| | | |
| | 取旧Activity的NCI(含ViewModelStore) | | |
| |-------------------------->| | | |
| | 销毁旧Activity(isChange=true) | | |
| |-------------------------->| | | |
| | 创建新Activity | | | |
| |------------------------------->| | | |
| | | | 调用ViewModelProvider(this) | |
| | | |-------------------------->| |
| | | | | 调用getViewModelStore() | |
| | | | |<-------------------------| |
| | 传旧NCI的ViewModelStore | | | |
| |------------------------------->| | |
| | | | | 查ViewModelStore有实例 | |
| | | | |<-------------------------| |
| | | | | 返回旧ViewModel | |
| | | |<--------------------------| |
| | | | 用ViewModel渲染界面 | |
| | | |------------------------------------------->|
| | | | | | |
场景 2:Activity finish(真搬家)→ 管家下岗
text
用户 系统 Activity ViewModelStore ViewModel
| | | | |
| 按返回键 | | | |
|----------->| | | |
| | 调用onPause/onStop | | |
| |-------------------------->| | |
| | 调用onDestroy(isChange=false) | | |
| |-------------------------->| | |
| | | 调用clear() | |
| | |----------------->| |
| | | | 调用onCleared() |
| | | |----------------->|
| | | | 清空HashMap |
| | | |----------------->|
| | 销毁Activity | | |
| |-------------------------->| | |
| | | | | GC回收ViewModel
| | | | |<-----------------|
| | | | |
五、必记 3 个关键点
- ViewModel 不丢数据,靠的是 ViewModelStore:它像个档案库,跨 Activity 实例(配置变更)保存 ViewModel;
- ViewModelStore 不丢,靠的是 NCI:系统在配置变更时,用 NCI 暂存 ViewModelStore;
- ViewModel 销毁,只在 "真搬家" 时:Activity finish 或被系统杀死(非配置变更),才会调用 onCleared ()。