Activity配置变化后ViewModel 的 “不死之谜”

要搞懂 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 件事:

  1. 销毁旧 Activity 前,把旧 Activity 的 ViewModelStore 装进 NCI;
  2. 系统暂时保存这个 NCI(不会随旧 Activity 销毁);
  3. 新 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 个关键点

  1. ViewModel 不丢数据,靠的是 ViewModelStore:它像个档案库,跨 Activity 实例(配置变更)保存 ViewModel;
  2. ViewModelStore 不丢,靠的是 NCI:系统在配置变更时,用 NCI 暂存 ViewModelStore;
  3. ViewModel 销毁,只在 "真搬家" 时:Activity finish 或被系统杀死(非配置变更),才会调用 onCleared ()。
相关推荐
夜晚中的人海3 小时前
【C++】智能指针介绍
android·java·c++
用户2018792831673 小时前
后台Activity输入分发超时ANR分析(无焦点窗口)
android
游戏开发爱好者84 小时前
BShare HTTPS 集成与排查实战,从 SDK 接入到 iOS 真机调试(bshare https、签名、回调、抓包)
android·ios·小程序·https·uni-app·iphone·webview
2501_916008894 小时前
iOS 26 系统流畅度实战指南|流畅体验检测|滑动顺畅对比
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_915106326 小时前
苹果软件加固与 iOS App 混淆完整指南,IPA 文件加密、无源码混淆与代码保护实战
android·ios·小程序·https·uni-app·iphone·webview
2501_915921436 小时前
iOS 26 崩溃日志解析,新版系统下崩溃获取与诊断策略
android·ios·小程序·uni-app·cocoa·iphone·策略模式
齊家治國平天下8 小时前
Android 14 Input 事件派发机制深度剖析
android·input·hal
2501_916013749 小时前
iOS 推送开发完整指南,APNs 配置、证书申请、远程推送实现与上架调试经验分享
android·ios·小程序·https·uni-app·iphone·webview
李艺为11 小时前
非预置应用使用platform签名并且添加了android.uid.system无法adb安装解决方法
android·adb