Jetpack ViewModel quita el capullo

prefacio

Serie de artículos Jetpack AAC:

¿Qué hay del ciclo de vida de Jetpack? ¿Todavía hígado?
Jetpack LiveData Hora de echar un vistazo a
Jetpack ViewModel

Los dos primeros artículos analizaron Lifecycle y LiveData, y este artículo se centrará en el análisis de ViewModel y la relación entre los tres. A través de este artículo, aprenderá:

1. ¿Por qué necesita ViewModel?
2. Cómo usar
ViewModel 3. Principio de ViewModel
4. Asociación Lifecycle/LiveData/ViewModel

1. ¿Por qué necesita ViewModel?

Cambios en los elementos de configuración para la recuperación de datos

El cambio de elemento de configuración de Android comúnmente utilizado es el cambio de pantalla horizontal y vertical. Cuando se cambian las pantallas horizontal y vertical, la actividad se reconstruirá y volverá a crear (xx)... onResume(). En este momento, la actividad es ya es una instancia completamente nueva, por lo que se reconstruirá la Actividad anterior. El ViewTree asociado.
Además del cambio de pantalla horizontal y vertical, otros cambios en los elementos de configuración también reconstruirán la Actividad.

imagen.png

La pregunta es: ¿cómo restaurar los datos vinculados por ViewTree antes?

Métodos tradicionales de recuperación de datos

Android onSaveInstanceState/onRestoreInstanceState originalmente tenía que entenderse de esta manera, se ha analizado, por lo que lo mencionaré brevemente aquí:

1. Guarde los datos relacionados con ViewTree en onSaveInstanceState.
2. Restaure los datos relacionados con ViewTree en onRestoreInstanceState.

A través de estos dos métodos, los datos se pueden restaurar cuando se cambia el elemento de configuración. Sin embargo, este enfoque también tiene inconvenientes:

1、onSaveInstanceState 用Bundle存储数据便于跨进程传递,因此其存储上限受限于Binder(1M),不能用于恢复较大的数据,比如Bitmap。
2、复杂的类需要实现Parcelable/Serializable 接口。
3、onSaveInstanceState 在onStop 之后调用,调用比较频繁。

ViewModel 能够实现数据恢复功能,也规避了以上问题。

UI 数据的统一管理

以前管理UI 数据的时候,我们一般会定义一个Model,再定义一个Manager对其进行统一管理,借助于ViewMode+LiveData,能够更优雅地管理数据。

综上,ViewModel 能够进行数据恢复以及UI 数据统一管理。

2、ViewModel 的使用方式

横竖屏切换数据恢复

说得ViewModel 很优秀的样子,有代码有真相,以横竖屏切换为例,看看如何使用它。
定义ViewModel:

public class MoneyViewModel extends ViewModel {
    private int money;
    private String name = "官方ViewModel";

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
复制代码

MoneyViewModel 继承自ViewModel。
作为比对,再定义一个纯粹的类:

public class MyViewModel {
    private int money;
    private String name = "我的ViewModel";

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
复制代码

两者唯一的差别就是是否继承自ViewModel。

xml里定义两个TextView以及一个Button。

imagen.png

当点击修改文本的时候,将上面两个TextView 修改,Activity里代码如下:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_model);

        //先new 出Provider,再获取ViewModel
        moneyViewModel = new ViewModelProvider(this).get(MoneyViewModel.class);
        myViewModel = new MyViewModel();

        TextView tvVM = findViewById(R.id.tv_vm);
        tvVM.setText(moneyViewModel.getName());

        TextView tvMyVM = findViewById(R.id.tv_my_vm);
        tvMyVM.setText(myViewModel.getName());

        Button btnChange = findViewById(R.id.btn_change);
        btnChange.setOnClickListener((v) -> {
            moneyViewModel.setName("官方 ViewModel 改变");
            tvMyVM.setText(moneyViewModel.getName());

            myViewModel.setName("我的 ViewModel 改变");
            tvVM.setText(myViewModel.getName());
        });
    }
复制代码

点击Button后,修改我的ViewModel和官方ViewModel,此时UI 刷新。随后,将屏幕旋转到横屏,再查看UI 展示。

tt0.top-380809.gif

可以看出,竖屏切换到横屏后,官方ViewModel改变了,我的ViewModel没有改变。
显而易见,ViewModel 能够在横竖屏切换后恢复数据。

3、ViewModel 原理掘地三尺

ViewModelStore 的获取

moneyViewModel、myViewModel 同样作为ViewModelActivity 的成员变量,ViewModelActivity 都重建了(重新New ViewModelActivity 实例),理论上来说成员变量也是重建了的,为啥moneyViewModel 可以保持数据呢?这也是我们要探究的ViewModel 原理。
从ViewModel 的创建开始分析:

moneyViewModel = new ViewModelProvider(this).get(MoneyViewModel.class);
复制代码

ViewModelProvider 顾名思义:ViewModel 的提供者。
构造函数的形参为:ViewModelStoreOwner,该接口的唯一方法:

ViewModelStore getViewModelStore();
复制代码

我们看到上面传入了this,也即是说咱们的ViewModelActivity实现了该接口。

    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }
复制代码

mFactory、mViewModelStore 为ViewModelProvider 成员变量。
mFactory 为创建ViewModel的工厂方法,mViewModelStore 为ViewModel的存储器,它是通过 ViewModelStoreOwner.getViewModelStore()获取的。

ViewModelActivity 继承自AppCompatActivity,进而继承自ComponentActivity,而ComponentActivity 实现了ViewModelStoreOwner 接口,实现方法如下:

#ComponentActivity.java
    public ViewModelStore getViewModelStore() {
        ...
        ensureViewModelStore();
        return mViewModelStore;
    }

    void ensureViewModelStore() {
        if (mViewModelStore == null) {
            //为空,从mLastNonConfigurationInstances 寻找
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                // 直接恢复
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                //没找到,则创建ViewModelStore
                mViewModelStore = new ViewModelStore();
            }
        }
    }
复制代码

可以看出,ViewModelProvider. mViewModelStore 来源于ComponentActivity.mViewModelStore,而ComponentActivity.mViewModelStore 的赋值有两个地方:

1、从Activity.mLastNonConfigurationInstances里获取。
2、全新创建,直接new ViewModelStore。

imagen.png

ViewModel 的获取

ViewModelProvider.get(MoneyViewModel.class)
复制代码
#ViewModelProvider.java

    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();
        if (canonicalName == null) {
            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
        }
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }

    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        //先从ViewModelStore里获取
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            if (mFactory instanceof ViewModelProvider.OnRequeryFactory) {
                ((ViewModelProvider.OnRequeryFactory) mFactory).onRequery(viewModel);
            }
            //viewModel 不为空,说明找到了ViewModel
            return (T) viewModel;
        } else {
            ...
        }
        if (mFactory instanceof ViewModelProvider.KeyedFactory) {
            //根据工厂创建ViewModel
            viewModel = ((ViewModelProvider.KeyedFactory) mFactory).create(key, modelClass);
        } else {
            viewModel = mFactory.create(modelClass);
        }
        //将ViewModel 存储到ViewModelStore 里
        mViewModelStore.put(key, viewModel);
        return (T) viewModel;
    }
复制代码

与ViewModelStore 来源类似,ViewModel 来源于两个地方:

1、从ViewModelStore 里获取。
2、通过反射构造新的实例。

新生成的ViewModel 将会存储到ViewModeStore里。
而ViewModeStore 就只有一个成员变量:

private final HashMap<String, ViewModel> mMap = new HashMap<>();
复制代码

此处的Map 的key 即为自定义ViewModel的全限定类名+前缀。

imagen.png

Activity 重建对ViewModel 的影响

由上面分析可知,ViewModelStore 被Activity 持有,而ViewModel 被ViewModelStore 持有。 屏幕从竖屏切换到横屏时,Activity 重建了,拿到的ViewModel 却没有变化,我们有理由相信ViewModelStore 没有变,而纵观ViewModelStore 赋值,此时的ViewModelStore 很有可能从NonConfigurationInstances里获取的。
接着分析 NonConfigurationInstances的来龙去脉。

需要注意的是,NonConfigurationInstances 在Activity.java和ComponentActivity.java 里都有定义。

#Activity.java
    public Object getLastNonConfigurationInstance() {
        return mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.activity : null;
    }
复制代码

重点查看mLastNonConfigurationInstances 的赋值,它的调用栈如下图:

imagen.png

performLaunchActivity() 在新建Activity和重建Activity 时都会调用,只是在新建Activity 调用时,最后的ActivityClientRecord.lastNonConfigurationInstances =null。

重点又流转到ActivityClientRecord,它是怎么确定的。

#ActivityThread.java
    public void handleRelaunchActivity(ActivityClientRecord tmp,
                                       PendingTransactionActions pendingActions) {
        ...
        //从mActivities 里获取
        //final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
        ActivityClientRecord r = mActivities.get(tmp.token);
        ...
        handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
                pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
    }
复制代码

现在看来有点眉目了,在重建Activity 时:

1、从mActivities(缓存)里寻找ActivityClientRecord。
2、通过ActivityClientRecord 找到lastNonConfigurationInstances。

接下来看看ActivityClientRecord.lastNonConfigurationInstances 在哪赋值的。 我们知道Activity 重建时的步骤:

1、先将原来的Activity 销毁。
2、再重新新建一个Activity实例。

而Activity 销毁时会调用

#ActivityThread.java
    ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
                                                int configChanges, boolean getNonConfigInstance, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        ...
        if (r != null) {
            activityClass = r.activity.getClass();
            ...
            if (getNonConfigInstance) {
                try {
                    //从Activity 里获取
                    r.lastNonConfigurationInstances
                            = r.activity.retainNonConfigurationInstances();
                } catch (Exception e) {
                    ...
                }
            }
            ...
        }
        ...
        synchronized (mResourcesManager) {
            //移除ActivityClientRecord
            mActivities.remove(token);
        }
        ...
        return r;
    }
复制代码

重点查看activity.retainNonConfigurationInstances():

#Activity.java
    NonConfigurationInstances retainNonConfigurationInstances() {
        //ComponentActivity 重写了该方法
        Object activity = onRetainNonConfigurationInstance();
        ...
        NonConfigurationInstances nci = new NonConfigurationInstances();
        //activity = ComponentActivity.NonConfigurationInstances
        nci.activity = activity;
        ...
        return nci;
    }
复制代码

接着看onRetainNonConfigurationInstance 方法。

#ComponentActivity.java
    public final Object onRetainNonConfigurationInstance() {
        ViewModelStore viewModelStore = mViewModelStore;
        ...
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        //存储ViewModeStore
        nci.viewModelStore = viewModelStore;
        return nci;
    }
复制代码

终于,又看到了ViewModeStore。
ComponentActivity.NonConfigurationInstances 存储了viewModelStore,然后ComponentActivity.NonConfigurationInstances 存储在Activity. NonConfigurationInstances.activity 里。

ViewModel 能够恢复的真正原因

重新归纳小结一下:

1、Activity 新建时,创建了ViewModeStore,而ViewModelStore 里存储了ViewModel。
2、竖屏切换为横屏时,先将当前Activity 销毁,此时会调用到performDestroyActivity(),该方法里将ViewModeStore 封装在NonConfigurationInstances里,而NonConfigurationInstances 最终赋值给 ActivityClientRecord.lastNonConfigurationInstances里。
3、ActivityClientRecord 实例存储在ActivityThread里的Map里(Activity 启动时存放的)。
4、Activity 销毁后,重建Activity时,通过ActivityThread的Map 找到之前被销毁的Activity关联的ActivityClientRecord,从中取出lastNonConfigurationInstances赋值给Activity.mLastNonConfigurationInstances。
5、在Activity.onCreate()里调用new ViewModelProvider(this).get(MoneyViewModel.class) 时,发现此时的ViewModelStore == null,于是从Activity.mLastNonConfigurationInstances里获取,此时就能够拿到上一次的ViewModeStore,进而获取到ViewModel。

以上步骤就是ViewMode 数据恢复的过程。

imagen.png

核心的地方在于:ViewModel 最终存储在ActivityThread里,而ActivityThread一直存在(和进程生命周期一致),与Activity 生命周期毫无关联。

这也就是为什么经常说ViewModel里不能持有Activity 的引用,因为ViewModel 的生命周期比Activity 长。

讲到这,你可能有疑惑了:ViewModel生命周期具体有多长呢?
ComponentActivity 里有通过Lifecycle监听Activity生命周期,当Activity 处在"ON_DESTROY"状态时,有如下判断:

#ComponentActivity.java
    if (event == Lifecycle.Event.ON_DESTROY) {
        if (!isChangingConfigurations()) {
            //如果配置项没有发生变更,则认为是Activity 正常销毁
            //清除ViewModelStore
            getViewModelStore().clear();
        }
    }
复制代码

最终将ViewModel从ViewModeStore 的Map里移除。
当我们重写ViewModel.onCleared()方法时,在ViewModel 被清除时将会被调用,用于一些资源的释放。

因此,ViewModel 具体的生命周期如下:

1、当配置项没发生变更时,ViewModel 随着Activity 销毁而销毁。
2、当配置项发生变更而导致Activity 重建时,ViewModel 生命周期长于Activity。

4、Lifecycle/LiveData/ViewModel 关联

ViewModel 优势

通篇分析下来,发现ViewModel.java 本身很简单,系统为了恢复ViewModel 做了很多工作。
优势

1、ViewModel 不限制类型,不限制大小。
2、没有onSaveInstanceState 保存/恢复数据时的缺陷。
3、ViewModel 配合LiveData,既可以分离UI和数据,又可以通过数据驱动UI。
4、ViewModel 通过Activity 获取,只要拿到Activity实例就有机会拿到ViewModel,因此可以作为多个Fragment的数据共享中转。

Lifecycle 与LiveData 关联

LiveData 借助Lifecycle 实现生命周期监听,判别活跃与非活跃状态等,实现一个有生命周期感知的数据。

ViewModel 与Lifecycle 关联

没啥关联,也没有配合使用。

ViewModel 与 LiveData 关联

ViewModel 分离了UI 和数据,想要通过数据驱动UI,得配合LiveData 使用更香。
如下简单示例:

public class LiveDataViewModel extends ViewModel {
    private MutableLiveData<String> mutableLiveData;

    public MutableLiveData<String> getLiveData() {
        if (mutableLiveData == null) {
            mutableLiveData = new MutableLiveData<>();
        }
        return mutableLiveData;
    }
}
复制代码

更多例子都在github 上,有兴趣可以查看。

在分析LiveData和ViewModel时,在一开始都没有将两者一并提出来,就是为了让大家能够清晰地认识到两者的职能边界,因为应用场景不一样,两者都可以单独使用,而对于配置项更改敏感的场景,两者结合会更加方便。

下篇将分析ViewModel,彻底厘清为啥ViewModel能够存储数据以及运用场合。

本文基于:implementation 'androidx.appcompat:appcompat:1.4.1' & Android 10.0

ViewModel 演示&工具

您若喜欢,请点赞、关注、收藏,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android

1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17. Serie
prebasada en Android Jetpack 18. Serie Android Jetpack fácil de aprender y comprender

Supongo que te gusta

Origin juejin.im/post/7082942171925446669
Recomendado
Clasificación