ViewModel原理分析

认识 ViewModel

ViewModel 是一种用来存储和管理UI相关数据的类。

ViewModel 的作用可以从两个方面去理解:

  • UI界面控制器:在最初的MVC模式中,由于 Activity / Fragment 承担的职责过重,因此在后续的 MVP、MVVM 模式中,选择将 Activity / Fragment 中与视图无关的职责抽离出来,在 MVP 模式中叫作 Presenter,在 MVVM 模式中叫作 ViewModel。使用 ViewModel 来承担界面控制器的职责,并且配合 LiveData / Flow 实现数据驱动。
  • 数据存储:由于 Activity 存在因配置变更销毁重建的机制,会造成 Activity 中的所有瞬态数据丢失,例如网络请求得到的用户信息、视频播放信息或者异步任务都会丢失。而 ViewModel 的特点是生命周期长于 Activity ,因此能够应对 Activity 因配置变更而重建的场景,在重建的过程中恢复 ViewModel 数据,从而降低用户体验受损。

ViewModel 生命周期示意图:
ViewModel 生命周期示意图

使用 ViewModel

使用步骤:

  1. 自定义 ViewModel 继承 ViewModel。
  2. 在自定义 ViewModel 中编写获取UI数据的逻辑。
  3. 配合使用 LiveData / Flow 实现数据驱动。
  4. 在 Activity / Fragment中 获取 ViewModel 实例。
  5. 监听或收集 ViewModel 中的 LiveData / Flow 数据,进行对应的UI更新。

简单示例:

class TestFlowViewModel : ViewModel() {private val _state: MutableStateFlow<Int> = MutableStateFlow(0)val state: StateFlow<Int> get() = _stateprivate val _live: MutableLiveData<String> = MutableLiveData<String>()val live: LiveData<String> get() = _livefun test() {_live.value = "1"for (state in 1..5) {viewModelScope.launch(Dispatchers.IO) {delay(100L * state)_state.emit(state)}}}
}

Activity / Fragment 中相关代码:

private val viewModel: TestFlowViewModel by viewModels()lifecycleScope.launch {launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.state.collect {Log.d(TAG, "state: $it ")}}}delay(100)viewModel.test()
}
viewModel.live.observe(this) {Log.d(TAG, "it: $it")
}

在 Activity / Fragment中 获取 ViewModel 实例有两种方式,一种是通过ViewModelProvider获取,也key自定义ViewModelProvider.Factory,

private val viewModel = ViewModelProvider(this).get(TestFlowViewModel::class.java)

另一种就是上面示例里面使用的方式:使用 Kotlin by 委托属性,本质上是间接使用了 ViewModelProvider。

@MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(noinline extrasProducer: (() -> CreationExtras)? = null,noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {val factoryPromise = factoryProducer ?: {defaultViewModelProviderFactory}return ViewModelLazy(VM::class,{ viewModelStore },factoryPromise,{ extrasProducer?.invoke() ?: this.defaultViewModelCreationExtras })
}public class ViewModelLazy<VM : ViewModel> @JvmOverloads constructor(private val viewModelClass: KClass<VM>,private val storeProducer: () -> ViewModelStore,private val factoryProducer: () -> ViewModelProvider.Factory,private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty }
) : Lazy<VM> {private var cached: VM? = nulloverride val value: VMget() {val viewModel = cachedreturn if (viewModel == null) {val factory = factoryProducer()val store = storeProducer()ViewModelProvider(store,factory,extrasProducer()).get(viewModelClass.java).also {cached = it}} else {viewModel}}override fun isInitialized(): Boolean = cached != null
}

分析 ViewModel 原理

ViewModel 创建过程

上面说到了创建 ViewModel 实例的方法最终都是通过 ViewModelProvider 完成的。ViewModelProvider 可以理解为创建 ViewModel 的工具类,它需要 2 个参数:

  • ViewModelStoreOwner: 它对应于 Activity / Fragment 等持有 ViewModel 的宿主,它们内部通过 ViewModelStore 维持一个 ViewModel 的映射表,ViewModelStore 是实现 ViewModel 作用域和数据恢复的关键;
  • Factory: 它对应于 ViewModel 的创建工厂,缺省时将使用默认的 NewInstanceFactory 工厂来反射创建 ViewModel 实例。

创建 ViewModelProvider 工具类后,通过 get() 方法来获取 ViewModel 的实例。get() 方法内部首先会从 ViewModelStore 中取缓存,没有缓存才会通过 ViewModel 工厂创建实例再缓存到 ViewModelStore 中。

    @MainThreadpublic open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {// 先从 ViewModelStore 中取缓存val viewModel = store[key]// 存在 ViewModel,直接返回if (modelClass.isInstance(viewModel)) {(factory as? OnRequeryFactory)?.onRequery(viewModel!!)return viewModel as T} else {@Suppress("ControlFlowWithEmptyBody")if (viewModel != null) {// TODO: log a warning.}}val extras = MutableCreationExtras(defaultCreationExtras)extras[VIEW_MODEL_KEY] = key// AGP has some desugaring issues associated with compileOnly dependencies so we need to// fall back to the other create method to keep from crashing.// 不存在则使用 ViewModel 工厂创建实例,并放入 ViewModelStorereturn try {factory.create(modelClass, extras)} catch (e: AbstractMethodError) {factory.create(modelClass)}.also { store.put(key, it) }}

ViewModelStore 是 ViewModel 存储器,内部通过 LinkedHashMap 存储 ViewModel。

ViewModelStoreOwner 是一个接口,ViewModelStore 的持有者是ViewModelStoreOwner 的实现类,包括有ComponentActivity和Fragment,它们内部都保存着一个 ViewModelStore。

interface ViewModelStoreOwner {/*** The owned [ViewModelStore]*/val viewModelStore: ViewModelStore
}
public class ComponentActivity extends androidx.core.app.ComponentActivity implementsContextAware,LifecycleOwner,ViewModelStoreOwner ... {private ViewModelStore mViewModelStore;private ViewModelProvider.Factory mDefaultFactory;@NonNull@Overridepublic ViewModelStore getViewModelStore() {...ensureViewModelStore();return mViewModelStore;}
}

正因为 ViewModel 宿主内部都保存着一个 ViewModelStore ,因此在同一个宿主上重复调用 ViewModelProvider#get() 返回同一个 ViewModel 实例,这也就做到了 fragment 共享 activity 的ViewModel 实例以及 fragment 之间共享 ViewModel。

为什么 Activity 在屏幕旋转重建后可以恢复 ViewModel?

上面说到 ViewModel 其实是被保存在 ViewModelStore 里,所以 Activity 在屏幕旋转重建后恢复 ViewModel 其实是重新获取到了原有的 ViewModelStore。那么 Activity 里的 ViewModelStore 究竟是怎么生成的呢?

ViewModelStore 的生成

ComponentActivity 在构造函数中设置了对 lifecycle 的监听,当 activity 的生命周期变化时会调用ensureViewModelStore方法,确保 ViewModelStore 的存在。

        getLifecycle().addObserver(new LifecycleEventObserver() {@Overridepublic void onStateChanged(@NonNull LifecycleOwner source,@NonNull Lifecycle.Event event) {ensureViewModelStore();getLifecycle().removeObserver(this);}});
    @SuppressWarnings("WeakerAccess") /* synthetic access */void ensureViewModelStore() {if (mViewModelStore == null) {NonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance();if (nc != null) {// Restore the ViewModelStore from NonConfigurationInstancesmViewModelStore = nc.viewModelStore;}if (mViewModelStore == null) {mViewModelStore = new ViewModelStore();}}}

当 ViewModelStore 不存在时,ensureViewModelStore 方法会进行 ViewModelStore 的生成,首先通过 getLastNonConfigurationInstance 获取到 NonConfigurationInstances,并从中获取 ViewModelStore,若 ViewModelStore 仍未空,则创建 new 一个新的。

看到这,知道了 ViewModelStore 的生成来源于两处,一处为 NonConfigurationInstances 中获取,另一处是新创建。那么 Activity 在屏幕旋转重建后获取到了原有的 ViewModelStore 是不是就是从 NonConfigurationInstances 中获取的呢?

接下去看一下 Activity 在屏幕旋转重建后,ViewModelStore 都干什么去了呢?

Activity 因配置变更而重建时(比如屏幕旋转),可以将页面上的数据或状态可以定义为 2 类:

  • 配置数据:例如窗口大小、多语言字符、多主题资源等,当设备配置变更时,需要根据最新的配置重新读取新的数据,因此这部分数据在配置变更后便失去意义,自然也就没有存在的价值;
  • 非配置数据:例如用户信息、视频播放信息、异步任务等非配置相关数据,这些数据跟设备配置没有一点关系,如果在重建 Activity 的过程中丢失,不仅没有必要,而且会损失用户体验(无法快速恢复页面数据,或者丢失页面进度)。
    基于以上考虑,Activity 是支持在设备配置变更重建时恢复非配置数据的,源码中存在 NonConfiguration 字眼的代码,就是与这个机制相关的代码。

当 Activity 因配置变更而重建时,ActivityThreadhandleRelaunchActivity 方法会执行,先 handleDestroyActivity 销毁 Activity,然后 handleLaunchActivity 重建 Activity。

Activity 销毁过程

在 handleDestroyActivity 方法里执行到 performDestroyActivity 时,会执行 activity 的 retainNonConfigurationInstances 方法,将非配置数据临时存储在当前 Activity 的 ActivityClientRecord(当前进程内存)。

    void performDestroyActivity(ActivityClientRecord r, boolean finishing,int configChanges, boolean getNonConfigInstance, String reason) {Class<? extends Activity> activityClass = null;if (localLOGV) Slog.v(TAG, "Performing finish of " + r);activityClass = r.activity.getClass();r.activity.mConfigChangeFlags |= configChanges;if (finishing) {r.activity.mFinished = true;}performPauseActivityIfNeeded(r, "destroy");if (!r.stopped) {callActivityOnStop(r, false /* saveState */, "destroy");}if (getNonConfigInstance) {try {// 将非配置数据临时存储在 ActivityClientRecordr.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();} catch (Exception e) {if (!mInstrumentation.onException(r.activity, e)) {throw new RuntimeException("Unable to retain activity "+ r.intent.getComponent().toShortString() + ": " + e.toString(), e);}}}...}
// 获取 Activity 的非配置相关数据
NonConfigurationInstances retainNonConfigurationInstances() {// 构造 Activity 级别的非配置数据Object activity = onRetainNonConfigurationInstance();...// 构造 Fragment 级别的费配置数据数据FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();...// 构造并返回 NonConfigurationInstances 非配置相关数据类NonConfigurationInstances nci = new NonConfigurationInstances();nci.activity = activity;nci.fragments = fragments;...return nci;
}// 默认返回 null,由 Activity 子类定义
public Object onRetainNonConfigurationInstance() {return null;
}

看一下onRetainNonConfigurationInstance在 ComponentActivity 中的实现:

    public final Object onRetainNonConfigurationInstance() {// Maintain backward compatibility.Object custom = onRetainCustomNonConfigurationInstance();ViewModelStore viewModelStore = mViewModelStore;if (viewModelStore == null) {// No one called getViewModelStore(), so see if there was an existing// ViewModelStore from our last NonConfigurationInstance// 这一个 if 语句是处理异常边界情况: // 如果重建的 Activity 没有调用 getViewModelStore(),那么旧的 Activity 中的 ViewModel 并没有被取出来, // 因此在准备再一次存储当前 Activity 时,需要检查一下旧 Activity 传过来的数据。NonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance();if (nc != null) {viewModelStore = nc.viewModelStore;}}// ViewModelStore 为空说明当前 Activity 和旧 Activity 都没有 ViewModel,没必要存储和恢复if (viewModelStore == null && custom == null) {return null;}NonConfigurationInstances nci = new NonConfigurationInstances();nci.custom = custom;// 保存 ViewModelStore 对象nci.viewModelStore = viewModelStore;return nci;}

看到了 ViewModelStore 实例的保存,就这样,当 Activity 被销毁时,ViewModelStore 实例被保存进了 NonConfigurationInstances 中,进而被临时存储在了 ActivityClientRecord 里。

Activity 重建过程

在 handleLaunchActivity 方法里执行到 performLaunchActivity 时,会执行 activity 的attach方法,并将 ActivityClientRecord中的 NonConfigurationInstances 传入。

// 在 Activity#attach() 中传递旧 Activity 的数据
NonConfigurationInstances mLastNonConfigurationInstances;final void attach(Context context, ActivityThread aThread,...NonConfigurationInstances lastNonConfigurationInstances) {...mLastNonConfigurationInstances = lastNonConfigurationInstances;...
}

至此,旧 Activity 的数据就传递到新 Activity 的成员变量 mLastNonConfigurationInstances 中了,ViewModelStore 将从 mLastNonConfigurationInstances 中获取。

ViewModel 数据清除

ViewModel 的数据又是在什么时候会被清除呢?

ViewModel 中有一个 clear 方法用于数据清除,在 ViewModelStore#clear() 方法中被调用。

上面提到 ViewModelStore 的生成是 ComponentActivity 在构造函数中设置了对 lifecycle 的监听,当 activity 的生命周期变化时会调用ensureViewModelStore方法,确保 ViewModelStore 的存在。
ComponentActivity 在构造函数中设置对 lifecycle 的监听可不只有这一处,ViewModel 数据的清除也是通过对 lifecycle 的监听,当 Activity 进入 destroyed 状态,并且 Activity 不处于配置变更重建的阶段,将调用 ViewModelStore#clear() 清除 ViewModel 数据。

        getLifecycle().addObserver(new LifecycleEventObserver() {@Overridepublic void onStateChanged(@NonNull LifecycleOwner source,@NonNull Lifecycle.Event event) {if (event == Lifecycle.Event.ON_DESTROY) {// Clear out the available contextmContextAwareHelper.clearAvailableContext();// And clear the ViewModelStoreif (!isChangingConfigurations()) {getViewModelStore().clear();}}}});

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/23471.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于JSP技术的人事管理系统

你好呀&#xff0c;我是计算机学长猫哥&#xff01;如果有相关需求&#xff0c;文末可以找到我的联系方式。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSP技术 工具&#xff1a;浏览器&#xff08;如360浏览器、谷歌浏览器、QQ浏览器等&#xff…

深度神经网络——什么是扩散模型?

1. 概述 在人工智能的浩瀚领域中&#xff0c;扩散模型正成为技术创新的先锋&#xff0c;它们彻底改变了我们处理复杂问题的方式&#xff0c;特别是在生成式人工智能方面。这些模型基于高斯过程、方差分析、微分方程和序列生成等坚实的数学理论构建。 业界巨头如Nvidia、Google…

【C语言】文件操作(下卷)

前言 在上一卷中&#xff0c;我们知道了文件指针、文件的打开和关闭&#xff08;打开其他位置的文件&#xff09;、文件的顺序读写&#xff08;其中的fputc()、fgetc()&#xff09;&#xff0c;这一卷中&#xff0c;将继续讲解文件操作未讲到的地方。 内容有点多&#xff0c;…

人大金仓数据库报sys_user表字段不存在的问题

目录 一.问题&#xff1a; 二.原因 三.解决方法&#xff1a; 一.问题&#xff1a; 公司的一个项目从oracle切换到人大金仓之后&#xff0c;突然报了一个sys_user里面的字段不存在。 二.原因 检查了很多次确信sys_user表没问题&#xff0c;查了相应的文档之后发现原来人大金…

企业自建邮件系统的优势,安全性更高,功能更灵活,维护更便捷

在当今企业信息管理的浪潮中&#xff0c;企业邮件系统显得尤为关键&#xff0c;它不仅加强了内部的沟通效率&#xff0c;还对外展示了企业的专业形象。然而&#xff0c;传统租用企业邮箱服务存在一些不足&#xff0c;如缺乏灵活性、数据管理混乱和难以实现个性化需求&#xff0…

Wireshark 如何查找包含特定数据的数据帧

1、查找包含特定 string 的数据帧 使用如下指令&#xff1a; 双引号中所要查找的字符串 frame contains "xxx" 查找字符串 “heartbeat” 示例&#xff1a; 2、查找包含特定16进制的数据帧 使用如下指令&#xff1a; TCP&#xff1a;在TCP流中查找 tcp contai…

HOW - 面试技巧系列 - 全英文面试

自我介绍 “can you tell me a little bit about yourself?” “please introduce yourself.” 工作经验 “can you describe your most recent job experience?” “how does your experience make you a good fit for this position?” 职业规划 “what are your l…

C语言---指针part2

指针操作 一维 字符数组 1. 字符型数组 --- 存放字符串的 char s[] "hello"; [h ] <---0x1000 [e ] [l ] [l ] [o ] [\0] //谁能这块空间的地址 --- 数组名 s --->怎么能保存s所代表的地址值 //s数组名 --- 数组首元素的地址 &s[0] --->地…

谷歌浏览器使用--disable-web-security --user-data-dir解决跨域的解释

–disable-web-security 和 --user-data-dir 是Google Chrome&#xff08;及其开源版本Chromium&#xff09;浏览器在启动时可以使用的命令行参数。下面我将分别解释这两个参数的含义和用途&#xff1a; 1.–disable-web-security 含义&#xff1a;这个参数用于禁用Chrome浏览…

服务器数据恢复—raid5阵列上层XFS文件系统数据恢复案例

服务器存储数据恢复环境&#xff1a; 某品牌CX4-480型号服务器存储&#xff0c;该服务器存储内有一组由20块硬盘组建的raid5磁盘阵列&#xff1b;存储空间分配了1个lun。 服务器存储故障&#xff1a; 工作人员将服务器重装操作系统后&#xff0c;未知原因导致服务器操作系统层…

9. 媒体查询与响应式设计

随着移动设备和不同尺寸屏幕的普及&#xff0c;响应式设计成为前端开发的重要组成部分。响应式设计确保网页在不同设备上都有良好的用户体验。本章将详细介绍媒体查询的基础知识、语法与使用方法&#xff0c;以及如何运用CSS3实现响应式设计。 9.1 媒体查询基础 媒体查询是CS…

Flink协调器Coordinator及自定义Operator

Flink协调器Coordinator及自定义Operator 最近的项目开发过程中&#xff0c;使用到了Flink中的协调器以及自定义算子相关的内容&#xff0c;本篇文章主要介绍Flink中的协调器是什么&#xff0c;如何用&#xff0c;以及协调器与算子间的交互。 协调器Coordinator Flink中的协调…

LlamaIndex 一 简单文档查询

前言 在学习LangChain的时候&#xff0c;我接触到了LlamaIndex。它犹如我在开发vue时用到的axios&#xff0c;主要负责数据打理。别问我为什么打这个比方&#xff0c;前端老狗&#xff0c;重走AI路&#xff0c;闭关一年能否学的妥当&#xff1f; LlamaIndex 是一个用于 LLM 应…

前端项目打包、部署的基础 (vue)

详细请看B站视频 BV19n4y1d7Gr 《禹神&#xff1a;前端项目部署指南&#xff0c;前端项目打包上线》&#xff0c;本博客为自用视频笔记。 目录 项目打包vue打包打包前分析项目请求 本地服务器部署问题 & 解决问题1&#xff1a;刷新页面404问题问题2&#xff1a;ajax请求废…

【人工智能】第六部分:ChatGPT的进一步发展和研究方向

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

秀肌肉-海外短剧系统的案例展示

多语种可以选择&#xff0c;分销功能&#xff0c;多种海外支付方式&#xff0c;多种登录模式可供选择&#xff0c;总之你想到的我们都做了&#xff0c;你没想到的我们也都做了

企微自动化机器人的应用与前景

一、引言 随着信息技术的飞速发展&#xff0c;企业对于提高内部运营效率、降低人力成本的需求日益迫切。在这样的背景下&#xff0c;企微自动化机器人应运而生&#xff0c;以其高效、便捷的特点&#xff0c;迅速成为企业内部的得力助手。本文将深入探讨企微自动化机器人的应用现…

头歌易-算式运算的合法性

给定一个算式运算,算式由运算数、+、-、*、/、(、)组成,请编写程序判断该算式运算是否合法。如果合法,计算该算式的值。 输入描述: 第一行输入一个运算表达式 输出描述: 如果表达式合法则计算其值,结果保留两位小数,如果不合法则输出 表达式不合法! 输入样例: (5+3)…

Partially Spoofed Audio Detection论文介绍(ICASSP 2024)

An Efficient Temporary Deepfake Location Approach Based Embeddings for Partially Spoofed Audio Detection 论文翻译名&#xff1a;一种基于部分欺骗音频检测的基于临时深度伪造位置方法的高效嵌入 摘要&#xff1a; 部分伪造音频检测是一项具有挑战性的任务&#xff0…

NSSCTF-Web题目6

目录 [NISACTF 2022]checkin 1、题目 2、知识点 3、思路 [NISACTF 2022]babyupload 1、题目 2、知识点 3、思路 [SWPUCTF 2022 新生赛]1z_unserialize 1、题目 2、知识点 3、思路 [NISACTF 2022]checkin 1、题目 2、知识点 010编辑器的使用、url编码 3、思路 打…