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() = _state
    private val _live: MutableLiveData<String> = MutableLiveData<String>()
    val live: LiveData<String> get() = _live
    
    fun 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? = null

    override val value: VM
        get() {
            val viewModel = cached
            return 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 中。

    @MainThread
    public 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 工厂创建实例,并放入 ViewModelStore
        return 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 implements
    ContextAware,
    LifecycleOwner,
    ViewModelStoreOwner ... {
		
    private ViewModelStore mViewModelStore;
    private ViewModelProvider.Factory mDefaultFactory;

    @NonNull
    @Override
    public 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() {
            @Override
            public 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 NonConfigurationInstances
                mViewModelStore = 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 {
                // 将非配置数据临时存储在 ActivityClientRecord
                r.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() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    // Clear out the available context
                    mContextAwareHelper.clearAvailableContext();
                    // And clear the ViewModelStore
                    if (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

springboot项目部署需要redis集群问题

本来直接将redis为单独启动模式转为配置 yml文件 spring.redis.cluster.nodes: 192.168.12.78:8001,192.168.12.78:8002,192.168.12.78:8003, java文件 package io.sirc.config;import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.ann…

三、【源码】Mapper XML的解析和注册使用

源码地址&#xff1a;https://github.com/mybatis/mybatis-3/ 仓库地址&#xff1a;https://gitcode.net/qq_42665745/mybatis/-/tree/03-parse-mapperXML Mapper XML的解析和注册使用 流程&#xff1a; 1.Resources加载MyBatis配置文件生成Reader字符流 2.SqlSessionFact…

UnityXR Interactable Toolkit如何实现Climb爬梯子

前言 在VR中,通常会有一些交互需要我们做爬梯子,爬墙的操作,之前用VRTK3时,里面是还有这个Demo的,最近看XRI,发现也除了一个爬的示例,今天我们就来讲解一下 如何在Unity中使用XR Interaction Toolkit实现爬行(Climb)操作 环境配置 步骤 1:设置XR环境 确保你的Uni…

Linux学习总结

单行注释:# 多行注释::<<! ! cd # 进入家目录 cd ~ # 进入家目录 cd / # 进入根目录 cd - # 返回上一次目录 cd .. # 进入上一级目录 cd ../.. # 进入上上级目录 # 写法一 [命令] & # 写法二 nohup [命令] & 两种写法都可以…

商城项目【尚品汇】06压力测试-性能指标-Jmeter使用-压力测试报告

文章目录 1.压测目的2.性能指标3.Jmeter3.1Jmeter使用3.1.1 运行Jmeter3.1.2 添加线程组3.1.3设置HTTP请求3.1.4 设置监视器 3.2 查看Jmeter压测结果3.2.1 查看结果树3.2.2 查看汇总报告3.2.3 查看聚合报告3.2.4 查看汇总图 1.压测目的 内存泄漏&#xff1a;OOM&#xff0c;重…

Nginx配置详细解释

文章目录 一、配置详细解释关闭版本修改启动的进程数cpu与work进程绑定nginx进程的优先级work进程打开的文件个数event事件 二、Http设置协议配置说明mime虚拟主机aliaslocationaccess模块验证模块自定义错误页面自定义日志存放位置try_files检测文件是否存在长连接 一、配置详…

【vue-admin-template】设置前后端访问地址

最近在使用vue-admin-template模板进行二次开发&#xff0c;GitHub地址&#xff1a; Vue-Admin-Template。 如果要在该项目中设置前后端的访问IP及端口&#xff0c;可以这样做&#xff1a; 前端&#xff1a;在vue.config.js中&#xff1a; 后端&#xff1a;在request.js中&…

CorelDRAW 全称“CorelDRAW Graphics Suite

箭头在各种场景中被广泛使用。在设计中&#xff0c;设计师可以根据设计的目的和受众&#xff0c;巧妙地运用箭头来传达信息、创造视觉效果或引导观者的注意力。在CDR软件中可以为设计添加箭头&#xff0c;那具体该怎么做呢&#xff1f;下面由我带大家一起来了解CoreIDRAW箭头形…

【SpringBoot + Vue 尚庭公寓实战】项目介绍(一)

【尚庭公寓SpringBoot Vue 项目实战】项目介绍&#xff08;一&#xff09; 文章目录 【尚庭公寓SpringBoot Vue 项目实战】项目介绍&#xff08;一&#xff09;1、项目业务概述2、移动端介绍3、 后台管理系统4、 核心业务流程5、项目技术概述5、数据库设计 1、项目业务概述 …

青否数字人直播源码超级管理后台操作步骤!

青否数字人直播源码超级管理后台&#xff0c;我们将详细介绍一下数字人的管理后台的详细操作步骤&#xff01; 1.管理端入口 2.管理后台预览 账号管理&#xff0c;模特管理&#xff0c;声音管理&#xff0c;任务管理&#xff0c;卡类管理&#xff0c;代理商&#xff0c;克隆端 …

【WP|9】深入解析WordPress [add_shortcode]函数

add_shortcode 是 WordPress 中一个非常强大的函数&#xff0c;用于创建自定义的短代码&#xff08;shortcodes&#xff09;。短代码是一种简洁的方式&#xff0c;允许用户在内容中插入动态的、可重用的功能。通过 add_shortcode&#xff0c;开发者可以定义自己的短代码&#x…

xstream运用,JAVA对象转xml,xml转JAVA对象

目录 xstream 优点&#xff1a; 缺点&#xff1a; XStream的应用场景 用到的依赖 代码实现 xml标签对应的实体类 Header Package Request Response TradeInfo 工具类 XmlUtils 执行结果 xstream XStream是一个Java类库&#xff0c;主要用于将对象序列化为XML&#xf…

Cochrane Library循证医学数据库的介绍及文献下载

今天要讲的数据库是Cochrane Library循证医学数据库&#xff0c;我们先来了解一下该数据库&#xff1a; Cochrane Library是国际Cochrane Collaboration的主要产品&#xff0c;由英国Wiley InterScience公司出版发行。是一个提供高质量证据的数据库&#xff0c;是循证医学的证…

如何在centos中关闭swap分区

目录 前言 为什么要关闭 Swap 分区&#xff1f; 如何在 CentOS 中临时关闭 Swap 分区&#xff1f; 如何在 CentOS 中永久关闭 Swap 分区&#xff1f; 验证swap是否被关闭 潜在的风险和注意事项 总结 前言 Swap 分区是 Linux 系统中用于扩展物理内存的一种机制。在物理内存…

vs code 导出插件 导入到新电脑上

1. 在 现在的电脑上 导出插件 在vscode 上执行 code --list-extensions > extensions.txt 然后项目的目录就有了一个文件 2. 将他复制到新电脑上&#xff0c;把文件放在项目的最外层&#xff08;跟上面的目录一样&#xff09; 执行命令 Get-Content extensions.txt | ForE…

华硕NUC 14 Pro+ :科技与艺术相得益彰

什么样的迷你主机可以称之为“艺术品”&#xff1f;让我们一起认识NUC 14 Pro&#xff0c;看科技与艺术可以交汇出怎样的独特韵味&#xff1f; 科技与美学的邂逅 华硕NUC 14 Pro不仅是一台性能强劲的电脑主机&#xff0c;更像是一件可以在桌面“展出”的艺术品。精致小巧的体积…

【C++进阶】深入STL之vector:深入研究迭代器失效及拷贝问题

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;C “ 登神长阶 ” &#x1f921;往期回顾&#x1f921;&#xff1a;初步了解vector &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀STL之vector &#x1f4d2;1. 迭…

聚焦热点-“十五五”规划 国家发改委前期研究课题汇总

聚焦热点-“十五五”规划 国家发改委前期研究课题汇总 随着“十五五”规划的脚步日益临近&#xff0c;国家发改委及地方相关机构已启动了前期研究工作&#xff0c;以确保地方规划能够准确把握时代脉搏&#xff0c;推动经济社会的高质量发展。 2023年12月17日至18日&#xff0…

10.爬虫---XPath插件安装并解析爬取数据

10.XPath插件安装并解析爬取数据 1.XPath简介2.XPath helper安装3.XPath 常用规则4.实例引入4.1 //匹配所有节点4.2 / 或 // 匹配子节点或子孙节点4.3 ..或 parent::匹配父节点4.4 匹配属性4.5 text()文本获取4.6 属性获取4.7 属性多值匹配 1.XPath简介 XPath是一门在XML文档中…

【WP】猿人学13_入门级cookie

https://match.yuanrenxue.cn/match/13 抓包分析 抓包分析发现加密参数是cookie中有一个yuanrenxue_cookie 当cookie过期的时候&#xff0c;就会重新给match/13发包&#xff0c;这个包返回一段js代码&#xff0c;应该是生成cookie的 <script>document.cookie(y)(u)(a…