Jetpack ViewModel 抽丝剥茧

前言

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 易懂易学系列

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容