美文网首页
深入剖析ViewModel:应对屏幕旋转、系统配置变动与进程回收

深入剖析ViewModel:应对屏幕旋转、系统配置变动与进程回收

作者: 野火友烧不尽 | 来源:发表于2025-03-04 15:26 被阅读0次

Jetpack之Lifecycle原理剖析:解锁高效组件生命周期管理
深入剖析Jetpack之LiveData原理:构建稳健高效的Android数据架构
Jetpack Compose 巅峰架构:ViewMde+Flow+Retrofit,重塑 Android 开发新格局
深入剖析ViewModel:应对屏幕旋转、系统配置变动与进程回收的数据管理之道

概述

在Android开发的广袤天地里,屏幕旋转、系统配置变动以及进程意外回收等情况,犹如隐藏在暗处的“小怪兽”,时不时给开发者们带来数据管理方面的挑战。尤其是Activity因这些情况而销毁重建时,如何确保数据不丢失,成为了开发者们必须攻克的难题。而ViewModel的出现,就像是手持魔法宝剑的勇士,巧妙地化解了这一困境。今天,就让我们一同揭开ViewModel在这些复杂场景下数据管理的神秘面纱。

一、问题的引出:那些令人头疼的场景

在日常使用手机应用的过程中,屏幕旋转是再常见不过的操作了。可别小瞧这个简单的动作,它却能在Android开发中掀起“波澜”。当进行屏幕旋转,或者切换系统语言、更改字体大小等系统配置变动时,Activity的生命周期会经历一次完整的从销毁到重建的过程。

想象一下,用户在一款笔记应用中精心编辑了一段文字,正准备点击保存,这时不小心旋转了屏幕,结果编辑的内容全部丢失,这体验得多糟糕!不仅如此,在某些极端情况下,系统可能会因为内存不足等原因回收应用进程,这同样会导致Activity销毁重建,数据的完整性面临巨大威胁。

但神奇的是,我们会发现ViewModel中的变量值在这些场景下却能“稳如泰山”,不受影响。这背后究竟隐藏着怎样的奥秘呢?接下来,让我们深入代码的世界一探究竟。

二、获取ViewModel实例:关键步骤解析

在代码里,获取ViewModel实例的常见代码如下:

// MainActivity.kt
val viewModel = ViewModelProvider.create(viewModelStore).get(MainViewModel::class)

为了更透彻地理解这行代码,我们把它拆分成两部分来分析:ViewModelProvider.create(viewModelStore).get(MainViewModel::class)

(一)ViewModelProvider.create(viewModelStore):实例获取的起点

ViewModelProvider.create(viewModelStore). 的主要职责是获取 ViewModelProvider 实例。我们来看看相关的源码:

// ViewModelProvider.kt
public constructor(
        store: ViewModelStore,
        factory: Factory,
        defaultCreationExtras: CreationExtras = CreationExtras.Empty
    ) : this(ViewModelProviderImpl(store, factory, defaultCreationExtras))

// ComponentActivity.kt   
 override val viewModelStore: ViewModelStore
        get() {
            checkNotNull(application) {
                ("Your activity is not yet attached to the " +
                    "Application instance. You can't request ViewModel before onCreate call.")
            }
            ensureViewModelStore()
            return _viewModelStore!!
        }

private fun ensureViewModelStore() {
        if (_viewModelStore == null) {
            val nc = lastNonConfigurationInstance as NonConfigurationInstances?
            if (nc != null) {
                // 从NonConfigurationInstances恢复ViewModelStore
                _viewModelStore = nc.viewModelStore
            }
            if (_viewModelStore == null) {
                _viewModelStore = ViewModelStore()
            }
        }
    }

从这些代码可以看出,最终调用的是 ComponentActivity 中的 ensureViewModelStore() 方法。这里面涉及到两个极为重要的类:ViewModelStoreNonConfigurationInstances

ViewModelStore,从名字就能猜到,它就像是一个“数据仓库”,专门用来存储ViewModel对象,起到缓存的作用,其底层是通过 HashMap 实现的。来看它的源码:

// ViewModelStore.java
public open class ViewModelStore {

    private val map = mutableMapOf<String, ViewModel>()

    public fun put(key: String, viewModel: ViewModel) {
        val oldViewModel = map.put(key, viewModel)
        oldViewModel?.clear()
    }

    /**
     * 返回与给定`key`映射的`ViewModel`,如果不存在则返回null。
     */
    public operator fun get(key: String): ViewModel? {
        return map[key]
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun keys(): Set<String> {
        return HashSet(map.keys)
    }

    /**
     * 清除内部存储,并通知`ViewModel`它们不再被使用。
     */
    public fun clear() {
        for (vm in map.values) {
            vm.clear()
        }
        map.clear()
    }
}

NonConfigurationInstancesComponentActivity 的一个内部类,虽然结构简单,但作用重大。它里面包含 viewModelStorecustom 等字段,代码如下:

// ComponentActivity$NonConfigurationInstances.kt
internal class NonConfigurationInstances {
        var custom: Any? = null
        var viewModelStore: ViewModelStore? = null
    }

(二)get(MainViewModel::class):实例的查找与创建

接着看 get(MainViewModel::class) 这部分代码:

// ViewModelProvider.kt
    public actual operator fun <T : ViewModel> get(modelClass: KClass<T>): T =
        impl.getViewModel(modelClass)

// ViewModelProviderImpl.kt
internal class ViewModelProviderImpl(
    private val store: ViewModelStore,
    private val factory: ViewModelProvider.Factory,
    private val extras: CreationExtras
) {

    constructor(
        owner: ViewModelStoreOwner,
        factory: ViewModelProvider.Factory,
        extras: CreationExtras
    ) : this(owner.viewModelStore, factory, extras)

    @Suppress("UNCHECKED_CAST")
    internal fun <T : ViewModel> getViewModel(
        modelClass: KClass<T>,
        key: String = ViewModelProviders.getDefaultKey(modelClass)
    ): T {
        val viewModel = store[key]
        if (modelClass.isInstance(viewModel)) {
            if (factory is ViewModelProvider.OnRequeryFactory) {
                factory.onRequery(viewModel!!)
            }
            return viewModel as T
        } else {
         if (viewModel != null) {
             
            }
        }
        val extras = MutableCreationExtras(extras)
        extras[ViewModelProviders.ViewModelKey] = key

        return createViewModel(factory, modelClass, extras).also { vm -> store.put(key, vm) }
    }
}

这部分代码的逻辑是,先根据 keyViewModelStore 中查找缓存的ViewModel。如果找到了,并且该ViewModel的类型符合要求,就直接返回这个实例;要是没找到,就会通过工厂方法创建一个新的ViewModel,然后把它存入 ViewModelStore 缓存起来。

在Activity中,我们一般没有设置自定义的 key,默认的 key 是一个常量。基于此,我们推测:在屏幕旋转前后,ViewModelStore 是同一个对象,获取到的 viewModel 也应该是同一份实例对象。为了验证这个推测,我们可以通过日志打印来查看:

val viewModel =   ViewModelProvider.create(viewModelStore).get(ComposeViewModel::class)
Log.i("TAG--> ", "viewModel--> $viewModel"  + "     viewModelStore--> $viewModelStore")

实际运行时,打印结果显示,屏幕旋转后,ViewModelStoreViewModel 的对象地址确实没有变化,这就证实了我们的猜测。

三、ViewModelStore的恢复与存储机制

(一)viewModelStore的恢复

mViewModelStore 的恢复过程在 ComponentActivityensureViewModelStore() 方法中:

// ComponentActivity.kt
override val viewModelStore: ViewModelStore
        get() {
            ensureViewModelStore()
            return _viewModelStore!!
        }
private fun ensureViewModelStore() {
        if (_viewModelStore == null) {
            val nc = lastNonConfigurationInstance as NonConfigurationInstances?
            if (nc != null) {
                // 从NonConfigurationInstances恢复ViewModelStore
                _viewModelStore = nc.viewModelStore
            }
            if (_viewModelStore == null) {
                _viewModelStore = ViewModelStore()
            }
        }
    }

可以看到,屏幕旋转后,Activity重建时会从 getLastNonConfigurationInstance() 方法中获取之前保存的 NonConfigurationInstances 实例对象,然后从中取出存储的 viewModelStore 对象。这也就意味着,getLastNonConfigurationInstance() 方法会在 onCreate() 方法之前调用。

(二)ViewModelStore的存储

既然屏幕旋转后 ViewModelStore 是从 NonConfigurationInstances 获取的,那么在屏幕旋转前,它肯定也是存放在这里的。查看相关代码可以发现:

// ComponentActivity.kt
final override fun onRetainNonConfigurationInstance(): Any? {
        val custom = onRetainCustomNonConfigurationInstance()
        var viewModelStore = _viewModelStore
        if (viewModelStore == null) {
            // 调用getViewModelStore(),检查上一个NonConfigurationInstance中是否有已存在的ViewModelStore
            val nc = lastNonConfigurationInstance as NonConfigurationInstances?
            if (nc != null) {
                viewModelStore = nc.viewModelStore
            }
        }
        if (viewModelStore == null && custom == null) {
            return null
        }

        // 这里进行存储
        val nci = NonConfigurationInstances()
        nci.custom = custom
        nci.viewModelStore = viewModelStore
        return nci
    }

由此可知,屏幕旋转前,数据是在 onRetainNonConfigurationInstance() 方法中保存的。

四、数据存储与恢复的深度探究

(一)Activity重启后数据的恢复

Activity重建后,数据是在 getLastNonConfigurationInstance() 中恢复的。相关源码如下:

// Activity.java
public Object getLastNonConfigurationInstance() {
        return mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.activity : null;
    }

mLastNonConfigurationInstances 的赋值是在 Activityattach() 方法中:

// Activity.java
final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        ···
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        ···
    }

从Activity的启动流程可知,Activity.attach() 方法是由 ActivityThread 调用的:

// ActivityThread.java

Activity.NonConfigurationInstances lastNonConfigurationInstances;

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    activity.attach(appContext, this, getInstrumentation(), r.token,
                    r.ident, app, r.intent, r.activityInfo, title, r.parent,
                    r.embeddedID, r.lastNonConfigurationInstances, config,
                    r.referrer, r.voiceInteractor, window, r.configCallback,
                    r.assistToken);    
}

这表明,数据实际上存储在 ActivityClientRecord 中。在Activity启动时,ActivityThread 会将 ActivityClientRecord 中的 lastNonConfigurationInstances 通过 attach() 方法传递给对应的Activity,然后Activity再通过 getLastNonConfigurationInstance() 方法恢复数据。

(二)屏幕旋转前数据的存储

屏幕旋转前,数据在 onRetainNonConfigurationInstance() 方法中保存,而这个方法是在 ActivityretainNonConfigurationInstances() 方法中被调用的。进一步查看发现,retainNonConfigurationInstances() 方法是在 ActivityThreadperformDestroyActivity() 方法中被调用的:

// ActivityThread.java    
ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
                                            int configChanges, boolean getNonConfigInstance, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    ···
        if (getNonConfigInstance) {
            try {
                r.lastNonConfigurationInstances
                    = r.activity.retainNonConfigurationInstances();
            } catch (Exception e) {
                ···
            }
        }
    ···
    return r;
}

可以看出,performDestroyActivity() 方法调用了 retainNonConfigurationInstances() 方法,并把数据保存到了 ActivityClientRecordlastNonConfigurationInstances 中。

五、特例:系统杀后台的情况

在实际开发过程中,我们通过实验发现了一个特殊情况:当系统杀后台时,Activity不会执行 onDestory()onRetainCustomNonConfigurationInstance() 方法。这就意味着Activity销毁的生命周期不会正常执行,只有当App再次回到前台时,才会执行Activity重建的生命周期。

由于没有执行 onRetainCustomNonConfigurationInstance() 方法,Activity的数据没有被缓存下来,所以Activity重建时也就没有数据可以恢复。从Activity中打印ViewModel实例对象的地址就能发现,使用 adb 模拟杀掉后台后,ViewModel的地址值发生了变化,变成了一个全新的地址,这说明原来的ViewModel实例已经不存在了。

六、总结

(一)屏幕旋转前,Activity销毁时

ComponentActivity 会调用 onRetainNonConfigurationInstance() 方法,将即将销毁的Activity的 ViewModelStore 转化为 NonConfigurationInstances 对象。接着,会继续调用 ActivityretainNonConfigurationInstances() 方法,最终在 ActivityThreadperformDestroyActivity() 方法中将数据保存在 ActivityClientRecord 中。

(二)Activity重建后

在Activity启动时,ActivityThread 会调用 performLaunchActivity() 方法,将存储在 ActivityClientRecord 中的 lastNonConfigurationInstances 通过 Activityattach() 方法传递到对应的Activity中。然后,Activity通过 getLastNonConfigurationInstance() 恢复 ViewModelStore 实例对象,最后根据对应的 key 拿到销毁前对应的 ViewModel 实例。

需要特别注意的是,当系统内存不足回收后台应用时,ViewModel中的数据默认是不会恢复的。在开发过程中,我们要根据实际业务需求,考虑是否需要额外的机制来处理这种情况,比如将关键数据持久化存储到本地文件或者数据库中。

通过以上对ViewModel在屏幕旋转、系统配置变动以及进程意外回收场景下数据管理原理的深入探究,相信大家对它的工作机制有了更清晰的认识。这不仅有助于我们在开发中更高效地利用ViewModel来管理数据,还能让我们在遇到相关问题时,能够迅速定位问题根源并找到解决方案。希望这篇文章能为大家的Android开发之路提供一些帮助!

相关文章

  • ViewModel浅析

    ViewModel用来可感知生命周期的方式存储和管理UI相关数据,当系统配置发生变更的时候,如屏幕旋转,数据不会丢...

  • ViewModel 你应该知道的知识点

    ViewModel 的 Saved State 在屏幕旋转时,ViewModel 可以保存数据。但是当应用在后台进...

  • ViewModel 使用及原理

    ViewModel 类旨在以注重生命周期的方式存储和管理界面相关数据。ViewModel 类可让数据在发生屏幕旋转...

  • Android ViewModel

    ViewModel注重以生命周期的方法存储和管理界面相关的数据,ViewModel类可在屏幕发生旋转等配置更改后仍...

  • ViewModel——Google组件开发

      ViewModel类旨在存储和管理与UI相关的数据,以便数据在诸如屏幕旋转之类的配置更改中生存下来。它还处理A...

  • ViewModel实现活动被系统异常销毁时的数据保存与恢复

    早期版本的ViewModel仅可实现应用在屏幕旋转等配置发生变化时保存与恢复数据,无法实现Activity在后台时...

  • ViewModel的原理

    众所周知,ViewModel的作用在于以可感知生命周期的方式存储与管理UI相关的数据,它允许数据在例如屏幕旋转这样...

  • 屏幕旋转

    屏幕旋转 推荐文档 了解UIWindow——UIWindow实践 iOS屏幕旋转问题总结 IOS:屏幕旋转与变换 ...

  • Android 官方架构组件(三)——ViewModel

    ViewModel类主要用来存储和管理与UI相关的数据,它能够让数据在屏幕旋转等配置信息改变导致UI重建的情况下不...

  • Jetpack入门(五)ViewModel介绍及原理

    前言 ViewModel可以感知activity的生命周期来管理UI相关的数据,在屏幕旋转后数据仍然存在。在传统的...

网友评论

      本文标题:深入剖析ViewModel:应对屏幕旋转、系统配置变动与进程回收

      本文链接:https://www.haomeiwen.com/subject/kdbrwktx.html