Jetpack之界面数据存储组件-ViewModel

2021年03月07日 99 字 Jetpack


ViewModel这个名称常常使人误解为MVVM中的ViewModel层,那么它是不是同一个东西呢?

ViewModel可以作为MVVM中的ViewModel层,但不是特指MVVM中的ViewModel层。

ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。

它是一个独立的组件,你可以把它视作生命周期相关的数据存储工具, 把它用在你觉得合适的任意地方,它可以是Fragment共享数据的桥梁、可以是屏幕方向切换时的数据来源、当然也可以是 MVVM 中 ViewModel层的 数据提供者,你可以使用LiveData让它变成可观察的数据,也可以使用DataBinding让他成为View的数据源

【本系列文章(JAVA/KOTLIN)演示案例均存储在github存储库ArchitecturalComponentExample中】

本篇所述源码及代码基于 API 30

引言

以往,我们在系统销毁或重新创建界面控制器时,为保证存储在其中的任何瞬态界面相关数据不丢失,往往使用Activity 可以使用 onSaveInstanceState() 方法从 onCreate() 中的捆绑包恢复其数据。此方法在一定场景下有可能不适用,例如:

此方法仅适合可以序列化再反序列化的少量数据,而不适合数量可能较大的数据,如用户列表或位图。

在这一方面上,ViewModel可以存储任意你想要存储的数据,相对于onSaveInstanceState() 显然是具有明显优势的。同时,它是一个生命周期敏感组件,对于像横竖屏切换类似情况,重建界面时,可以不必做类似重建位图、重新反序列化数据、重新请求网络数据的操作。

从规范上来说,ViewModel推荐我们将数据从界面层中拿出去:

从界面控制器逻辑中分离出视图数据所有权的操作更容易且更高效。

ViewModel使用篇

此篇所讲述的使用篇均仅介绍ViewModel自身特性,区别于官方文档对于ViewModel使用与LiveData等组件混在一起讲的情景。

引入依赖

1
implementation "androidx.lifecycle:lifecycle-viewmodel:2.3.0"

创建案例

首先,简单的,我们创建一个测试页面,一个计数Button以及一个输入框用以做数据的输入和变化,界面比较简单,此处不做赘述。

我们使操作上述控件改变响应的值并存储在Activity中:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class MainActivity extends AppCompatActivity {

public static final String TAG = "ViewModelDemoLog";

public int count = 0;

public String input = "";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Button btnCounter = findViewById(R.id.button);
EditText etInput = findViewById(R.id.editText);

btnCounter.setText(String.format("UP COUNT (%s)", count));

Log.i(TAG, "当前计数值:" + count);

btnCounter.setOnClickListener(v -> {
count++;
btnCounter.setText(String.format("UP COUNT (%s)", count));
Log.i(TAG, "计数增至:" + count);
});

etInput.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {

}

@Override
public void afterTextChanged(Editable s) {
input = s.toString();
Log.i(TAG, "输入框输入:" + s);
}
});

findViewById(R.id.button2).setOnClickListener(v -> startActivity(new Intent(MainActivity.this, SecActivity.class)));

}

}

这种情况下运行,点击计数Button日志:

1
2
3
4
当前计数值:0
计数增至:1
计数增至:2
计数增至:3

此时,翻转屏幕后日志:

1
2
当前计数值:0
输入框输入:

我们观察到通过点击button改变的count值在翻转屏幕后被清除了,这里我们简单验证了翻转屏幕时屏幕重建,Activity中保存的瞬时量数据不会被保存(真的不会吗)。这里我们发现多了一行的日志,似乎EditText在屏幕重建后被填充了一次值,不如我们在翻转屏幕前为EditText输入一个值再试一下:
翻转屏幕前:

1
2
3
4
5
6
当前计数值:0
计数增至:1
计数增至:2
输入框输入:a
输入框输入:aa
输入框输入:aaa

翻转屏幕后:

1
2
当前计数值:0
输入框输入:aaa

我们发现输入框的数据被保存下来并在重建屏幕时被重新赋值,需要关注此部分原理请见【附录】。

回到正题,那么,上述数据如果保存在ViewModel中会怎样呢?

使用ViewModel改造案例

首先,创建我们的ViewModel,并把需要存储的数据放到ViewModel中:

1
2
3
4
5
6
7
8
9
10
11
public class HomeDataViewModel extends ViewModel {

public int count = 0;

public String logText = "==start==";

public void appendLog(String log) {
logText += "\n" + log;
}

}

为避免EditText问题影响效果观测,后续流程将会移除EditText。

代码比较简单,只需要编写一个类继承ViewModel,并将你需要存储的数据放在ViewModel中即可。

而使用ViewModel代码也相对简单,你只需要通过ViewModelProvider(activity).get(HomeDataViewModel.class)即可获取到ViewModel对象。至于为何需要使用这样一个相对复杂的方式获取ViewModel,我们稍后在原理篇中具体说明。

当然你可能在想,如果我的ViewModel需要使用有参构造器怎么办?
不用担心,它也为我们提供了解决办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyViewModelFactory implements ViewModelProvider.Factory {

private String defaultConfig = "";

public MyViewModelFactory(String defaultConfig) {
this.defaultConfig = defaultConfig;
}

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return (T) new HomeDataViewModel(defaultConfig);
}
}

// 使用时
viewModel = new ViewModelProvider(this,
new MyViewModelFactory("i have default config"))
.get(HomeDataViewModel.class);

即创建一个 ViewModelProvider.Factory 为ViewModelProvider提供ViewModel的初始化操作,将ViewModel需要的参数传值给 MyViewModelFactory ,由MyViewModelFactory在创建 ViewModel时调用对应的构造器方法即可。

接下来我们改造Activity代码,使其使用ViewModel作为数据存储:

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
public class SecActivity extends AppCompatActivity {

public static final String TAG = "ViewModelDemoLog";

private HomeDataViewModel viewModel;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sec);

Button btnCounter = findViewById(R.id.button);

viewModel = new ViewModelProvider(this).get(HomeDataViewModel.class);

Log.i(TAG, "读取viewModel.hashCode(): " + viewModel.hashCode());

btnCounter.setText(String.format("UP COUNT (%s)", viewModel.count));

Log.i(TAG, "当前计数值:" + viewModel.count);

btnCounter.setOnClickListener(v -> {
viewModel.count++;
btnCounter.setText(String.format("UP COUNT (%s)", viewModel.count));
Log.i(TAG, "计数增至:" + viewModel.count);
});

}
}

这里我们直接将先前使用Activity中变量改为使用ViewModel中的变量即可,就是这么简单,那么我们来看一下其效果:

翻转屏幕前:

1
2
3
4
5
读取viewModel.hashCode(): 210932679
当前计数值:0
计数增至:1
计数增至:2
计数增至:3

翻转屏幕后:

1
2
读取viewModel.hashCode(): 210932679
当前计数值:3

我们再点击两次计数按钮:

1
2
计数增至:4
计数增至:5

我们发现,我们的count字段值被保存了下来,而且翻转屏幕前后的ViewModel是同一个对象。

既然翻转前后的ViewModel是同一个对象,那么,可预见地,我们放在其内部的对象都不会被改变。也就是说,你可以在其中获取网络数据、缓存网络图片等等,而不用担心在屏幕翻转等改变Activity配置时,你的操作被中断以及重新创建时被重复调用。

关于更多ViewModel使用的案例,请点击此处参见官方文档

ViewModel的使用相对简单,其职责单一,注重生命周期,使用篇就介绍到这儿,我们来看看其源码及工作原理吧。

ViewModel生命周期

首先,ViewModel作为一个以注重生命周期的方式存储和管理界面相关的数据的组件,它的生命周期是怎样的呢?

这里我们引用官网对于ViewModel 的生命周期介绍的一张图来说明:
viewmodel_lifecycle.png

您通常在系统首次调用 Activity 对象的 onCreate() 方法时请求 ViewModel。系统可能会在 Activity 的整个生命周期内多次调用 onCreate(),如在旋转设备屏幕时。ViewModel 存在的时间范围是从您首次请求 ViewModel 直到 Activity 完成并销毁。

那么,让我们看看其具体是怎么做到的吧!

首先我们先从ViewModel对象的获取开始看:

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
@MainThread
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 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);
}
// 存入ViewModelStore
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}

这里主要利用ViewModelStore做了缓存(存储),缓存中有则取出,无则创建并加入缓存。

ViewModelStore内部维护了一个HashMap存储ViewModel:

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
public class ViewModelStore {

private final HashMap<String, ViewModel> mMap = new HashMap<>();

final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}

final ViewModel get(String key) {
return mMap.get(key);
}

Set<String> keys() {
return new HashSet<>(mMap.keySet());
}

/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
// 记住此处,稍后会讲
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}

代码比较简单,此处不做赘述,值得注意的是,我们在它的注释中发现了这样一句话:

Use {@link ViewModelStoreOwner#getViewModelStore()} to retrieve a {@code ViewModelStore} for activities and fragments.

我们查看一下 ViewModelStoreOwner :

1
2
3
4
5
6
7
8
9
public interface ViewModelStoreOwner {
/**
* Returns owned {@link ViewModelStore}
*
* @return a {@code ViewModelStore}
*/
@NonNull
ViewModelStore getViewModelStore();
}

只有一个 getViewModelStore的定义,查看其实现类,找到androidx.activity.ComponentActivity的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public ViewModelStore getViewModelStore() {
if (getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the "
+ "Application instance. You can't request ViewModel before onCreate call.");
}
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// Restore the ViewModelStore from NonConfigurationInstances
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}

我们发现,ViewModelStore来自于 NonConfigurationInstances ,而 NonConfigurationInstances 则取自 getLastNonConfigurationInstance(),我们先看一下 NonConfigurationInstances :

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

static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
}
}

public class Activity{

static final class NonConfigurationInstances {
Object activity;
HashMap<String, Object> children;
FragmentManagerNonConfig fragments;
ArrayMap<String, LoaderManager> loaders;
VoiceInteractor voiceInteractor;
}

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

由上述两段代码可知,ViewModelStore最终会存储在 Activity.NonConfigurationInstances中的 activity字段中,我们继续追溯Activity.NonConfigurationInstances mLastNonConfigurationInstances;是从哪里获取和取值的:

1
2
3
4
5
6
7
8
9
10
11
12
13
@link{Activity#attach}
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.performLaunchActivity()中被调用:

1
2
3
4
5
6
7
8
9
10
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.lastNonConfigurationInstances 从ActivityClientRecord中取得
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);
// ... 忽略其他代码
}

不难发现,lastNonConfigurationInstances来自于ActivityClientRecord对象,我们仅关注配置变化时的Activity重建流程,继续追溯,我们一次可以向前找到一下调用:

  • ActivityThread.handleLaunchActivity(ActivityClientRecord r ,…)
  • ActivityThread.handleRelaunchActivityInner(ActivityClientRecord r ,…)
  • ActivityThread.handleRelaunchActivity(ActivityClientRecord r ,…)
  • ClientTransactionHandler.handleRelaunchActivity(ActivityThread.ActivityClientRecord r,…)
  • ActivityRelaunchItem.excute(ClientTransactionHandler client,…)
  • ActivityConfigurationChangeItem.excute(ClientTransactionHandler client,…)

我们观察Activity重建的重建流程,只是对当前Activity对应的ActivityClientRecord进行了转换, 新创建的Activity的ActivityClientRecord依旧持有原Activity的lastNonConfigurationInstances对象。

那么这就最终保证了NonConfigurationInstances中的ViewModelStore在配置更改Activity重建过程中不会被改变,那么存储在其中的ViewModel自然也就没有变化。

前面我们了解了ViewModel的存储原理,其主要依赖于ActivityClientRecord的特性来存储NonConfigurationInstances对象,保证ViewModel存在,而界面销毁时ActivityClientRecord也会被销毁,那么保存在其中的NonConfigurationInstances及其包含的ViewModel自然也就会被销毁。

当然销毁的时候,也会通知到我们自定义的ViewModel,用以做内存及资源的回收,具体实现为重写 onCleared()方法:

1
2
3
4
5
6
7
8
public class HomeDataViewModel extends ViewModel {

@Override
protected void onCleared() {
super.onCleared();
// 做流的关闭,资源回收等
}
}

向前追溯可以看到调用此方法的实为ViewModel.clear()方法【此处懒得贴代码】,
在之前的ViewModelStore代码中,我们观察到有以下代码:

1
2
3
4
5
6
7
public final void clear() {
for (ViewModel vm : mMap.values()) {
// 调用ViewModel.clear()
vm.clear();
}
mMap.clear();
}

继续追溯,你会发现,他会在ComponentActivity构造方法中被调用,具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ComponentActivity() {
// 忽略其他代码
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();
}
}
}
});
// 忽略其他代码
}

这里创建并注册了生命周期观察者,在Lifecycle.Event.ON_DESTROY事件(界面销毁)时调用ViewModelStore.clear();

至此,ViewModel的使用及工作原理,我们就大致梳理了一遍了~!

附录

EditText控件数据存储与恢复

在ViewModel以前,Activity重建数据恢复主要依靠Activit.onSaveInstanceState, 我们大胆猜测EditText数据在翻转屏幕时数据依旧存在与此有关,我们尝试跟进一下其保存数据的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void onSaveInstanceState(@NonNull Bundle outState) {
// 获取Window层级
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());

outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
if (mAutoFillResetNeeded) {
outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
getAutofillManager().onSaveInstanceState(outState);
}
// 分发Activity存储状态
dispatchActivitySaveInstanceState(outState);
}

首先EditText作为View显然是依存在Window下的,那么我们尝试从Window下手,先来看看这里调用的mWindow.saveHierarchyState(), 由于这里是抽象方法,所以我们去看PhoneWindow.saveHierarchyState():

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
@Override
public Bundle saveHierarchyState() {
Bundle outState = new Bundle();
if (mContentParent == null) {
return outState;
}

SparseArray<Parcelable> states = new SparseArray<Parcelable>();
// 收集父容器中的层级状态
mContentParent.saveHierarchyState(states);
outState.putSparseParcelableArray(VIEWS_TAG, states);

// 从容器中取得有焦点的控件
final View focusedView = mContentParent.findFocus();
if (focusedView != null && focusedView.getId() != View.NO_ID) {
// 存储控件ID
outState.putInt(FOCUSED_ID_TAG, focusedView.getId());
}

// save the panels
SparseArray<Parcelable> panelStates = new SparseArray<Parcelable>();
savePanelState(panelStates);
if (panelStates.size() > 0) {
outState.putSparseParcelableArray(PANELS_TAG, panelStates);
}

if (mDecorContentParent != null) {
SparseArray<Parcelable> actionBarStates = new SparseArray<Parcelable>();
mDecorContentParent.saveToolbarHierarchyState(actionBarStates);
outState.putSparseParcelableArray(ACTION_BAR_TAG, actionBarStates);
}

return outState;
}

获取容器View层级及状态mContentParent.saveHierarchyState(states)最终会调用:

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
@CallSuper
@Nullable protected Parcelable onSaveInstanceState() {
mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
if (mStartActivityRequestWho != null || isAutofilled()
|| mAutofillViewId > LAST_APP_AUTOFILL_ID) {
BaseSavedState state = new BaseSavedState(AbsSavedState.EMPTY_STATE);

if (mStartActivityRequestWho != null) {
state.mSavedData |= BaseSavedState.START_ACTIVITY_REQUESTED_WHO_SAVED;
}

if (isAutofilled()) {
state.mSavedData |= BaseSavedState.IS_AUTOFILLED;
}

if (mAutofillViewId > LAST_APP_AUTOFILL_ID) {
state.mSavedData |= BaseSavedState.AUTOFILL_ID;
}

state.mStartActivityRequestWhoSaved = mStartActivityRequestWho;
state.mIsAutofilled = isAutofilled();
state.mHideHighlight = hideAutofillHighlight();
state.mAutofillViewId = mAutofillViewId;
return state;
}
return BaseSavedState.EMPTY_STATE;
}

PhoneWindow.saveHierarchyState()的作用是:获取容器中所有View的层级及状态,然后筛选出面板panels和具有焦点的控件返回给Activity,EditText自然就被筛选记录,并被outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState())记录下来,最终被onSaveInstanceState()存储,至于onSaveInstance的具体原理此处不做讨论,有兴趣可自行深挖。