概述

前面我们讲过了lifecycle的使用及原理。今天我们谈谈viewModel。原本使用和原理是准备分开写的,结果我看了下ViewModel的原理,很简单,所以决定把两者放在一起了。那么接下来,我们进入正题。

ViewModel是什么?

ViewModel旨在以注重生命周期的方式存储和管理界面相关的数据。我们知道当屏幕旋转时,Activity会销毁并且重建,而它让数据可在发生屏幕旋转等配置更改后继续留存。

哎?那就有人要问了,为什么我们不通过onSaveInstanceState()对数据进行保存,然后在onCreate()的时候读取数据呢?这种方法其实只适合少量的数据,并且它还需要进行序列化操作。不过毕竟Bundle的传输数据是有大小限制的。

还有Activity和Fragment有数据交互的时候,那么我们的成本其实也是相对有点高。而ViewModel便可以替我们解决此类问题。

所以从UI控制器逻辑中分离出View的展示数据所有权的操作更容易且更高效。

ViewModel的生命周期

我们先看一张官网的图:

image

上图说明了Activity经历屏幕旋转而后结束时所处的各种生命周期状态。并且在Activity生命周期旁边显示了对应的ViewModel的生命周期。此图只展示了Activity相关的生命周期,而在Fragment上其实一样。

通常来说,我们获取一个ViewModel是在Activity的onCreate()中去获取的,但onCreate()方法可能被调用多次,比如屏幕旋转,所以ViewModel的存在时间其实是第一次获取实例到当前页面完全销毁。

ViewModel的使用

那么现在我们准备用ViewModel写一个demo。既然前面说了ViewModel存在的时间是第一次创建到页面完全销毁。那么我们就以屏幕旋转的场景为例。

在Activity中使用

既然我们需要验证ViewModel是否真的可以在屏幕旋转的时候存储数据,那么我们就以计时器为例,先不使用ViewModel,看看结果如何,接下来我们上代码,首先写一个简单的计时器的Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @date:2021/2/22
* @author:Silence
* @describe:
**/
class TimeCounter : LifecycleObserver {

private var canCount = false
private var count = 0

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private fun start() {
canCount = true
Thread {
while (canCount) {
Log.i(TAG, "start: $count")
Thread.sleep(1000)
count++
}
}.start()
}

@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private fun stop() {
canCount = false
Log.i(TAG, "stop: ")
}

companion object {
private const val TAG = "TimeCounter"
}
}

可以看到,我们在页面OnResume的时候开始计时,在页面Stop的时候停止计时。然后我们在activity中绑定下这个观察者,我们运行下,验证下效果:

上图可见,当我们在屏幕旋转的时候,因为页面的生命周期重新执行了,导致了计时器的数据也被重新初始化了。那么我们怎么用ViewModel解决这个问题呢?首先我们先写一个ViewModel。代码如下:

1
class MyViewModel(var count: Int = 0) : ViewModel()

然后我们在Activity中把当前的ViewModel定义一下,代码如下:

1
private val viewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }

然后我们给TimeCounter新增一个入参,在添加观察者时把当前的ViewModel传进去即可。代码如下:

1
lifecycle.addObserver(TimeCounter(viewModel))

把原先的count修改为viewModel.count即可。那么现在我们在看一下效果:

哎,可以了。接下来我们需要做的是,在不同场景我们可能需要不同的入参。那如何向ViewModel内部传参呢。别急,ViewModel向我们提供了一个Factory。我们可以通过这个工厂类来解决上述问题。直接上代码:

1
2
3
4
5
class MyViewModelFactory(var count: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MyViewModel(count) as T
}
}

既然定义了这个工厂类,那我们怎么使用呢?不慌,ViewModelProvider也提供了Factory相关参数,只需要把定义修改成如下代码:

1
private val viewModel by lazy { ViewModelProvider(this, MyViewModelFactory(10)).get(MyViewModel::class.java) }

这样就可以解决了初始值的问题了。我们先来看一下效果:

哎,解决了。不过,等等,我直接new一个ViewModel把初始值传进去不就行了吗?为什么还要写一个工厂类,搞这么麻烦干嘛。但是如果通过new ViewModel的方法进行传值的话,它就与Activity的生命周期绑定了,所以,切记,不要使用新建传值的方法去定义初始值。

在Fragment中使用

在日常开发中,Activity和Fragment通信是一个很常见的问题,需要通过定义相关接口去处理。此外,这两个Fragment都必须处理另一个Fragment尚未创建或不可见的情况。那么我们通过共享Activity的ViewModel来解决上述问题。那么接下来,我们依旧直接上代码,我们只需要在Fragment中这样定义ViewModel就可以了:

1
private val viewModel by lazy { activity?.let { ViewModelProvider(this).get(MyViewModel::class.java) } }

需要注意的是,我们定义的时候需要传入的是Activity的上下文,而不是Fragment的。
此方法具有以下优势:

  • Activity不需要执行任何操作,也不需要对此通信有任何了解。
  • 除了ViewModel约定之外,Fragment不需要相互了解。如果其中一个Fragment消失,另一个Fragment将继续照常工作。
  • 每个Fragment都有自己的生命周期,而不受另一个Fragment的生命周期的影响。如果一个Fragment 替换另一个Fragment,界面将继续工作而没有任何问题。

ViwModel实现原理

既然需要看ViewModel的原理,我们回过头去看下ViewModel生命周期的那张图,可以看出Activity完全销毁后才调用了ViewModel的onCleared方法。那我们使用反推法,看看ViewModel的onCleared方法是何时调用的,先上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public abstract class ViewModel {

@Nullable
private final Map<String, Object> mBagOfTags = new HashMap<>();
private volatile boolean mCleared = false;

@SuppressWarnings("WeakerAccess")
protected void onCleared() {
}

@MainThread
final void clear() {
mCleared = true;
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
closeWithRuntimeException(value);
}
}
}
onCleared();
}

...
}

可以看到在ViewModel的clear方法中调用了onCleared方法,那么我们看看clear方法是在哪里调用的。

1
2
3
4
5
6
7
8
9
10
11
public class ViewModelStore {

...

public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}

可以看到是在ViewModelStore的clear方法里面调用的,那么继续向上追踪,可以发现在ComponentActivity中调用了,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});

可以看到当activity调用了OnDestory并且isChangingConfigurations不成立的时候,会去调用ViewModelStore的clear方法。那我们就知道了为什么单单调用了onDestory,ViewModel的实例还存在的原因。那么我们看下isChangingConfigurations这个方法是用来干嘛的。

由源码可知,如果在onStop()中发现isChangingConfigurations()的返回值为false,则说明该Activity被暂停了,暂时不需要使用该资源了,则可以释放引用的资源;如果isChangingConfigurations()返回值为true,则说明该Activity正在被销毁然后重新创建一个新的,这种情况下引用的资源还需要马上用到(在新创建的Activity中),这样可以先不释放该资源,当新的Activity创建好后,则可以立即使用该资源。

我们使用反推法证实了在Activity完全结束后ViewModel的销毁才会执行。那么ViewModel的创建呢?话不多说,我们直接看get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@NonNull
@MainThread
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);
}

@SuppressWarnings("unchecked")
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
} else {
viewModel = mFactory.create(modelClass);
}
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}

可以看出它先从ViewModelStore获取ViewModel实例。如果获取到了就直接返回。如果未获取到就直接通过工厂类创建一个,然后放入mViewModelStore中去。所以我们知道了,即使Activity重新创建了,因为ViewModel没有销毁,所以之前存储在ViewModel的数据源还在。这就合理的解释了,为什么ViewModel可以解决屏幕旋转后页面数据存储的问题。

总结

本文主要介绍了ViewModel的使用以及原理,小结下,ViewModel在Activity首次onCreate的时候创建,并存入ViewModelStore,后续就算多次调用了onCreate方法,它永远都是读取上次在ViewModelStore存入的ViewModel实例。在Activity完全销毁后,调用ViewModel的onCleared方法将其清除。

参考

官网