ViewModel实例什么时候被回收

news2024/12/23 17:57:37

作者:TechMix

一、ViewModel存在的意义?

ViewModel做为JetPack中重要的组件,翻译成中文就是“视图模型”,根据分离关注点原则,ViewModel的出现,主要是为了分担Activity中的职责,专门用于存放和界面相关的数据。只要是在界面上能看到的数据,它的相关变量都应该存放在ViewModel中,而不是Activity中。 如果ViewModel作用只是将原本存放在Activity/Fragment中的数据拆分出去,那岂不是跟普通的Java类没啥两样? 其实ViewModel更重要的是:

  • 实现数据共享和跨模块通信,ViewModel的存储是以Activity的class为key存放的,就可在有Activity上下文的不同子模块获取共享的ViewModel对象;
  • Configuration改变后Activity重建后将销毁前的数据再attach到视图中;
  • ViewModel中提供了生命周期感知(LiveData+Lifecycle)和协程(ViewModel本身定义了ViewModelScope协程作用域)的支持;

二、怎么创建?

第一种方法(已弃用,不建议使用):ViewModelProviders.of().get()

val viewModel1 = ViewModelProviders.of(this).get(TestClearViewModel::class.java)

该方式已经被官方弃用,不再建议使用,其实跟踪ViewModelProviders.of()方法实现,内部就是new了一个ViewModelProvider对象。

第二种方法(第一种方案衍生出来的):自己创建ViewModelProvider实例,调用其get()方法,传入ViewModel的class对象

获取代码
val viewModel = ViewModelProvider(this).get(TestClearViewModel::class.java)
弊端

在同一个Activity的上下文,在不同的模块(类,即使同一个功能模块也会按单一职责拆分代码)中,想要获取共享的ViewModel实例会new多个ViewModelProvider()对象。

第三种方法(kt委托):

val viewModel3 by viewModels<TestClearViewModel>()

使用该方法获取ViewModel实例,需要添加以下依赖:

// ps: 版本选取适合自己项目的
implementation 'androidx.activity:activity-ktx:1.6.0'

实现代码:

@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 }
    )
}

最终实现还是通过new一个ViewModelProvider传入ViewModelProvider.Factory对象的方式创建的ViewModel对象,不同的是通过kt的Lazy机制,实现了一层缓存逻辑;

public class ViewModelLazy<VM : ViewModel> @JvmOverloads constructor(
    private val viewModelClass: KClass<VM>,
    private val storeProducer: () -> ViewModelStore, // 由Activity、Fragment实现ViewModelStoreOwner接口后提供
    private val factoryProducer: () -> ViewModelProvider.Factory,
    private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty }
) : Lazy<VM> {
    private var cached: VM? = null

    override val value: VM
        get() {
        	// 其实在ViewModelStore中已经有一层HashMap实现的缓存了,这里又增加一层缓存。
            val viewModel = cached
            return if (viewModel == null) {
                val factory = factoryProducer()
                val store = storeProducer()
                // 核心实现还是ViewModelProvider构造方法中传入ViewModelProvider.Factory对象
                ViewModelProvider(
                    store,
                    factory,
                    extrasProducer()
                ).get(viewModelClass.java).also {
                    cached = it
                }
            } else {
                viewModel
            }
        }

    override fun isInitialized(): Boolean = cached != null
}
  • 从上面的代码中看到viewModels()方法是做为ComponentActivity的拓展方法实现的;
  • 懒加载按需创建ViewModel实例
  • ViewModelProvider.Factory使用的是ComponentActivity中的默认实现defaultViewModelProviderFactory

三、生命周期?什么时候被销毁?

网上很多关于ViewModel的说法,有类似“ViewModel的生命周期是长于Activity的”这样的说法,其实是容易让人产生误解的。 下面进行测试验证一下,理解一下ViewModel的存活周期。 测试代码很简单,在Activity中创建一个自定义的ViewModel

// TestClearViewModel.kt的实现:
class TestClearViewModel : ViewModel() {
    var userId = MutableLiveData<String>()

    override fun onCleared() {
        super.onCleared()
        Log.d(MainActivity2.TAG, "onCleared: $this")
    }
}

// MainActivity2.kt的实现代码:
class MainActivity2 : AppCompatActivity() {
    companion object {
        const val TAG = "MainActivity_TAG"
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        
        val viewModel by viewModels<TestClearViewModel>()
        Log.d(TAG, "onCreate viewModel: $viewModel")
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        Log.d(TAG, "onConfigurationChanged: ")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy: ")
    }
}
  • case1 : 正常的按back键finish掉Activity,再重建Activity:
// 按back键之后的log:
com.yanggui.mvvmdemo2 D/MainActivity_TAG: onCreate viewModel: com.yanggui.mvvmdemo2.TestClearViewModel@86439b1
com.yanggui.mvvmdemo2 D/MainActivity_TAG: onCleared: com.yanggui.mvvmdemo2.TestClearViewModel@86439b1
com.yanggui.mvvmdemo2 D/MainActivity_TAG: onDestroy: 

// 重现点击桌面图标,重启Activity之后的log:
com.yanggui.mvvmdemo2 D/MainActivity_TAG: onCreate viewModel: com.yanggui.mvvmdemo2.TestClearViewModel@8e96463

从上面的log可以看到,ViewModel是跟随Activity的销毁而销毁的,Activity重启之后ViewModel也会新建一个实例。

  • case2 : 旋转屏幕之后finish掉Activity,引起的Activity重建:
// Activity在点击桌面图标,应用启动时创建的ViewModel实例:
com.yanggui.mvvmdemo2 D/MainActivity_TAG: onCreate viewModel: com.yanggui.mvvmdemo2.TestClearViewModel@c26ae96

// 第一次旋转屏幕引起Activity重建:
com.yanggui.mvvmdemo2 D/MainActivity_TAG: onDestroy: 
com.yanggui.mvvmdemo2 D/MainActivity_TAG: onCreate viewModel: com.yanggui.mvvmdemo2.TestClearViewModel@c26ae96

// 第二次旋转屏幕引起Activity重建:
com.yanggui.mvvmdemo2 D/MainActivity_TAG: onDestroy: 
com.yanggui.mvvmdemo2 D/MainActivity_TAG: onCreate viewModel: com.yanggui.mvvmdemo2.TestClearViewModel@c26ae96

通过上述实际验证,旋转屏幕其实ViewModel不会重建。

根据上述实际代码验证,也能说明google官方这张流程图:

    1. 正常情况的onDestroy,ViewModel会跟随Activity销毁而销毁;
    1. 异常情况下的onDestroy,ViewModel不会跟随Activity销毁而销毁,因为要在Activity异常销毁之后重建时用来根据ViewModel的数据恢复界面;

3.1 旋转屏幕之后ViewModel为什么不会重建?

ViewModel是由ViewModelStore类管理的,在其内部维护了一个HashMap,key是androidx.lifecycle.ViewModelProvider.DefaultKey:+ ViewModel的Class对象的canonicalName拼接而成的,value就是ViewModel对象。

// ViewModelStore.java:
public class ViewModelStore {
	// ViewModelStore存储多个ViewModel实例,用HashMap存放
    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);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}
  • ViewModelStore和ViewModelStoreOwner的关系 ViewModelStoreOwner其实就是常见的Fragment、ComponentActivity(实现了ViewModelStoreOwner)

  • ComponentActivity#getViewModelStore():
    @NonNull
    @Override
    public ViewModelStore getViewModelStore() {
        if (getApplication() == null) {
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        if (mViewModelStore == null) {
        	//(1)
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
            	//(2)
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;
            }
            //(3)
            // 直接new了一个ViewModelStore用户管理一个ViewModel Map集合
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
        return mViewModelStore;
    }

(1)这一步很关键,mViewModelStore用来管理ViewModel的map集合,但是如果mViewModelStore只是作为ComponentActivity的一个成员变量,那Activity销毁重建后,mViewModelStore也一定会销毁重建。在(3)下面的代码中也可以看到mViewModelStore是在getViewModelStore()中直接new出来的。

  • Fragment#getViewModelStore(): 转调FragmentManager#getViewModelStore(),最终在FragmentManagerViewModel中维护了一个Map<String, ViewModelStore>,用于存放多个Fragemnt对应的ViewModel实例,以who变量为key去获取ViweModel实例;
    @NonNull
    @Override
    public ViewModelStore getViewModelStore() {
        if (mFragmentManager == null) {
            throw new IllegalStateException("Can't access ViewModels from detached fragment");
        }
        return mFragmentManager.getViewModelStore(this);
    }

当ViewModel被销毁时会回调其onCleared()方法,直接跟进调用点发现,在ComponentActivity的无参构造方法中有如下代码:

    public ComponentActivity() {
        // ... 
        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();
                    // 在这里做ViewModel的销毁逻辑
                    if (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });
    // ...
}

从如上代码可以得出结论: 非异常情况(改变Configuration等)下的Activity销毁,ViewModel是在Activity的onDestroy生命周期时销毁。

3.2 isChangingConfigurations()的定义

// 以下代码是在Activity.java中定义的:
boolean mChangingConfigurations = false;
public boolean isChangingConfigurations() {
    return mChangingConfigurations;
}

在Activity和Activity的子类中都没有找到有赋值的地方,因为修饰符是package的,于是在其相同包名下,通过Find in files查找到是在ActivityThread.java的handleRelaunchActivity()中赋值为true的。

	// ActivityThread.java,sdk 33
    @Override
    public void handleRelaunchActivity(ActivityClientRecord tmp, PendingTransactionActions pendingActions) {
    	// ...
		r.activity.mConfigChangeFlags |= configChanges;
        r.mPreserveWindow = tmp.mPreserveWindow;
		// 这里会在屏幕旋转Activity重建时执行,将activity.mChangingConfigurations赋值为true
        r.activity.mChangingConfigurations = true;
        // ...
     }

以上只是验证了Activity.isChangingConfigurations()==true的方式不会调用getViewModelStore().clear()去清理当前Activity关联的ViewModel实例的HashMap集合。

屏幕旋转Activity会发生onDestroy()->onCreate重建的情况,重建的Activity是这么关联到之前的ViewModel的Map集合的?

3.3 结论

  • 1、手机旋转屏幕时Activity会被重建,但是ViewModel实例不会重新创建,由此可见ViewModel的生命周期是长于Activity的仅在非正常退出的情况成立; ps:上述现象是清单文件中Activity节点不配置android:configChanges="orientation|screenSize"属性的情况下会走onDestroy->onCreate流程,Activity被重建。
<activity
    android:name=".MainActivity2"
    android:exported="true"
    android:configChanges="orientation|screenSize"> // 注:两个属性都要写上才会不走重建Activity
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
  • 2、只有当Activity正常退出时才会跟着Activity一起销毁; 正常退出:非ConfigurationChange造成的Activity重建。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

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

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

相关文章

谷歌云 | Document AI 引入了强大的新自定义文档拆分器来自动化文档处理

【本文由Cloud Ace整理发布。Cloud Ace是谷歌云全球战略合作伙伴&#xff0c;拥有 300 多名工程师&#xff0c;也是谷歌最高级别合作伙伴&#xff0c;多次获得 Google Cloud 合作伙伴奖。作为谷歌托管服务商&#xff0c;我们提供谷歌云、谷歌地图、谷歌办公套件、谷歌云认证培训…

Orange:一个基于 Python 的数据挖掘可视化平台

本篇介绍一个适合初学者入门的机器学习工具。 Orange 简介 Orange 是一个开源的数据挖掘和机器学习软件。Orange 基于 Python 和 C/C 开发&#xff0c;提供了一系列的数据探索、可视化、预处理以及建模组件。 Orange 拥有漂亮直观的交互式用户界面&#xff0c;非常适合新手进…

测试用例(3)

功能测试用例方法&#xff1a; 等价类 边界值 因果图 正交实验分解法 判定表驱动分析方法 非功能性的测试用例方法&#xff1a; 错误推测法 功能图分析方法 场景&#xff1a; 场景设计方法 1) 在任何情况下都必须使⽤边界值分析⽅法&#xff0c;经验表明⽤这种⽅法设…

CGT Asia嘉年华|3D细胞培养与类器官研发峰会10月广州召开

类器官指利用成体干细胞或多能干细胞进行体外三维&#xff08;3D&#xff09;培养而形成的具有一定空间结构的组织类似物&#xff0c;是近10年来干细胞领域发展最快的研究热点之一。2022年&#xff0c;FDA 通过现代化法案 2.0&#xff0c;批准全球首个完全基于“类器官芯片”研…

pandas 笔记:pivot_table 数据透视表\pivot

1 基本使用方法 pandas.pivot_table(data, valuesNone, indexNone, columnsNone, aggfuncmean, fill_valueNone, marginsFalse, dropnaTrue, margins_nameAll, observedFalse, sortTrue)2 主要参数 dataDataFramevalues要进行聚合的列index在数据透视表索引&#xff08;index…

# Linux下替换删除文件中的颜色等控制字符的方法

Linux下替换删除文件中的颜色等控制字符的方法 文章目录 Linux下替换删除文件中的颜色等控制字符的方法1 Linux下的控制字符&#xff08;显示的文字并不是他本身&#xff09;&#xff1a;2 颜色字符范例&#xff1a;3 替换4 最后 我们在shell编程显示输出时&#xff0c;会定义文…

【Java】一个简单的接口例子(帮助理解接口+多态)

要求&#xff1a; 请实现笔记本电脑使用USB鼠标、USB键盘的例子 1. USB 接口&#xff1a;包含打开设备、关闭设备功能 2. 笔记本类&#xff1a;包含开机功能、关机功能、使用 USB 设备功能 3. 鼠标类&#xff1a;实现 USB 接口&#xff0c;并具备点击功能 4. 键盘类&am…

人才输送|我的“捷码低代码工程师”转型之路!

最新职位 招聘岗位&#xff1a;低代码工程师 需求公司&#xff1a;上海北斗西虹桥基地 应聘条件&#xff1a;1、本科以上&#xff0c;最好硕士&#xff1b;2、有三年以上开发经验&#xff1b; 工作内容&#xff1a;带领团队用捷码低代码平台进行项目开发。 工作地点&#xff1a…

长城汽车Hi4技术品牌成果初现,大象转身必将势不可挡

今年1-6月&#xff0c;长城汽车销售52万辆&#xff0c;新能源车型销售9.3万辆&#xff0c;同比增长47%&#xff1b;智能新能源新产品密集上市&#xff0c;新能源销量与占比逐月攀升&#xff0c;6月销售超2.6万辆&#xff0c;占比突破25%&#xff0c;皆创历史新高&#xff1b; 全…

文档翻译成中文怎么弄?今天分享文档翻译免费要怎么弄

有一天&#xff0c;小华来到了一个外国小镇。然而&#xff0c;他发现镇上的路牌、菜单和旅游手册都是用外语写的&#xff0c;让他感到非常困扰。他不知道该去哪里游玩&#xff0c;也无法理解当地的文化和历史。他非常喜欢这个小镇的风景&#xff0c;但是他无法读懂他们这里的一…

【Linux -- 查看进程--ps,top,pstree】

Linux – 查看进程 文章目录 Linux -- 查看进程一、查看进程 -- ps二、动态查看进程的变化 -- top三、pstree -- 可以找进程之间的相关性总结 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、查看进程 – ps 更详细的可以通过man ps查看 ps aux …

深入理解Java虚拟机(三)垃圾收集器与内存分配策略

Java与C之间有一堵由内存动态分配和垃圾收集技术所围成的高墙&#xff0c;墙外面的人想进去&#xff0c;墙里面的人却想出来。 Java内存运行时区域的各个部分&#xff0c;其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生&#xff0c;随线程而灭&#xff0c;栈中的栈帧随…

echarts图例对齐

富文本不生效&#xff0c;是没有设置lineHeight

【运维】shell监控脚本结合钉钉机器人实现服务及服务器监控告警

文章目录 前言一、监控shell脚本和钉钉机器人二、创建钉钉机器人&#xff1a;1.在钉钉群聊里点击设置2.在设置里点击机器人选项3.再点击添加机器人4.再点击选择自定义机器人5.设置机器人名称、是否加密、是否限制ip、以及触发关键字6.获取机器人的Webhook地址 三、编写监控脚本…

MySQL表关联更新

背景&#xff1a; 有两张表&#xff0c;一张class信息表&#xff0c;一张student信息表&#xff0c;但student表里的信息存在错误&#xff0c;需要用class表中的信息去更新student表数据。 方法一&#xff1a; update student_info s set class_name (select class_name fr…

一本通12951917:装箱问题

不知道说什么废话好了 题目 装箱问题 描述 有一个箱子容量为V&#xff08;正整数&#xff0c;0&#xff1c;&#xff1d;V&#xff1c;&#xff1d;20000&#xff09;&#xff0c;同时有n个物品&#xff08;0&#xff1c;n&#xff1c;&#xff1d;30)&#xff0c;每个物品…

Spring 的依赖注入

Spring 的依赖注入 文章目录 Spring 的依赖注入每博一文案1. 依赖注入1.1 构造注入1.1.1 通过参数名进行构造注入1.1.2 通过参数的下标&#xff0c;进行构造注入1.1.3 不指定参数下标&#xff0c;不指定参数名字&#xff0c;通过自动装配的方式 1.2 set 注入 2. set注入的各种方…

uniapp 小程序 picker 日期时间段选择(精确到年月日时分+周几)

效果图&#xff1a; picker时间选择器 精确到年月日时分周几 需要引入moment.js&#xff0c;有可能引入后在项目内会报错&#xff0c;可以考虑把选择日期作为一个组件引入 1、timepage.vue组件封装 <template><view><picker mode"multiSelector" :va…

人才输送|捷码帮我走出求职迷茫期!

大家好&#xff0c;我是边文军。 很荣幸应余老师之邀&#xff0c;在这里和各位兄弟姐妹分享应聘青岛英哲低代码工程师的求职经历&#xff0c;希望能给大家提供点帮助。 01 自我介绍 先来做一个自我介绍吧&#xff01;我是计算机专业出身&#xff0c;学的移动应用开发专业。学…

【贪心算法part01】| 455.分发饼干、376.摆动序列、53.最大子序和

目录 &#x1f388;LeetCode455.分发饼干 &#x1f388;LeetCode376.摆动序列 &#x1f388;LeetCode53.最大子序和 &#x1f388;LeetCode455.分发饼干 链接&#xff1a;455.分发饼干 假设你是一位很棒的家长&#xff0c;想要给你的孩子们一些小饼干。但是&#xff0c;…