Jetpack ViewModel 抽丝剥茧

1,546 阅读8分钟

前言

Jetpack AAC 系列文章:

Jetpack Lifecycle 该怎么看?还肝否?
Jetpack LiveData 是时候了解一下了
Jetpack ViewModel 抽丝剥茧

前两篇分析了Lifecycle和LiveData,本篇将着重分析ViewModel及其三者的关联。 通过本篇,你将了解到:

1、为什么需要ViewModel ?
2、ViewModel 的使用方式
3、ViewModel 原理掘地三尺
4、Lifecycle/LiveData/ViewModel 关联

1、为什么需要ViewModel ?

配置项更改到数据恢复

Android 配置项更改常用的即是横竖屏切换,当横竖屏切换的时候,Activity 会重建,重新走onCreate(xx)...onResume(),此时Activity 已经是全新的实例了,因此之前的Activity 关联的ViewTree 将会重建。
除了横竖屏切换,其它的配置项更改也会重建Activity。

image.png

问题来了:之前ViewTree 绑定的数据如何恢复呢?

传统的数据恢复方法

Android onSaveInstanceState/onRestoreInstanceState 原来要这么理解 已经分析过,此处再简单提一下:

1、在onSaveInstanceState 里保存ViewTree 相关的数据。
2、在onRestoreInstanceState 里恢复ViewTree 相关的数据。

通过这两个方法,在配置项更改的时候可以将数据恢复。不过这种方式也有缺陷:

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。

image.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。

image.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的全限定类名+前缀。

image.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 的赋值,它的调用栈如下图:

image.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 数据恢复的过程。

image.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、Android Jetpack 前置基础系列
18、Android Jetpack 易学易懂系列