1、从一个 Bug 说起
想必有过一定开发经验的同学对 ViewModel 都不会陌生,它是 Google 推出的 MVVM 架构模式的一部分。这里它的基础使用我们就不介绍了,毕竟这种类型的文章也遍地都是。今天我们着重来探讨一下它的生命周期。
起因是这样的,昨天在修复程序中的 Bug 的时候遇到了一个异常,是从 ViewModel 中获取存储的数据的时候报了空指针。我启用了开发者模式的 “不保留活动” 之后很容易地重现了这个异常。出现错误的原因也很简单,相关的代码如下:
private ReceiptViewerViewModel viewModel;
@Override
protected void doCreateView(Bundle savedInstanceState) {
viewModel = ViewModelProviders.of(this).get(ReceiptViewerViewModel.class); // 1
handleIntent(savedInstanceState);
// ...
}
private void handleIntent(Bundle savedInstanceState) {
LoadingStatus loadingStatus;
if (savedInstanceState == null) {
loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
}
viewModel.setLoadingStatus(loadingStatus);
}
在方法 doCreateView()
中我获取了 viewModel
实例,然后在 handleIntent()
方法中从 Intent
中取出传入的参数。当然,还要使用 viewModel
的 getter
方法从其中取出 loadingStatus
并使用。在使用的时候抛了空指针。
显然,一般情况下是不会出现问题的,但是如果 Activity 在后台被销毁了,那么再重建的时候就会出现空指针异常。
解决方法也比较简单,在 onSaveInstanceState()
方法中将数据缓存起来即可,即:
private void handleIntent(Bundle savedInstanceState) {
LoadingStatus loadingStatus;
if (savedInstanceState == null) {
loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
} else {
loadingStatus = (LoadingStatus) savedInstanceState.get(Router.RECEIPT_VIEWER_LOADING_STATUS);
}
viewModel.setLoadingStatus(loadingStatus);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(Router.RECEIPT_VIEWER_LOADING_STATUS, viewModel.getLoadingStatus());
}
现在的问题是 ViewModel 的生命周期问题,有人说在 doCreateView()
方法的 1 处得到的不是之前的 ViewModel 吗,数据不是之前已经设置过了吗?所以,这牵扯 ViewModel 是在什么时候被销毁和重建的问题。
2、ViewModel 的生命周期
有的人希望使用 ViewModel 缓存 Activity 的信息,然后在 doCreateView()
方法的 1 处得到之前的 ViewModel 实例,这样 ViewModel 的数据就是 Activity 销毁之前的数据,这可行吗?我们从源码角度来看下这个问题。
首先,每次获取 viewmodel
实例的时候都会调用下面的方法来获取 ViewModel 实例。从下面的 get()
方法中可以看出,实例化过的 ViewModel 是从 mViewModelStore
中获取的。如果由 ViewModelStores.of(activity)
方法得到的 mViewModelStore
不是同一个,那么得到的 ViewModel 也不是同一个。
下面方法中的 get()
方法中后续的逻辑是如果之前没有缓存过 ViewModel,那么就构建一个新的实例并将其放进 mViewModelStore
中。这部分代码逻辑比较简单,我们不继续分析了。
// ViewModelProviders#of()
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
ViewModelProvider.AndroidViewModelFactory factory =
ViewModelProvider.AndroidViewModelFactory.getInstance(activity);
return new ViewModelProvider(ViewModelStores.of(activity), factory); // 1
}
// ViewModelProvider#get()
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}
viewModel = mFactory.create(modelClass);
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
我们回到上述 of()
方法的 1 处,来看下 ViewModelStores.of()
方法,其定义如下:
// ViewModelStores#of()
public static ViewModelStore of(@NonNull FragmentActivity activity) {
if (activity instanceof ViewModelStoreOwner) {
return ((ViewModelStoreOwner) activity).getViewModelStore();
}
return holderFragmentFor(activity).getViewModelStore();
}
// HolderFragment#holderFragmentFor()
public static HolderFragment holderFragmentFor(FragmentActivity activity) {
return sHolderFragmentManager.holderFragmentFor(activity);
}
这里会从 holderFragmentFor()
方法中获取一个 HolderFragment
实例,它是一个 Fragment 的实现类。然后从该实例中获取 ViewModelStore
的实例。所以,ViewModel 对生命周期的管理与 Glide 和 RxPermission 等框架的处理方式一致,就是使用一个空的 Fragment 来进行生命周期管理。
对于 HolderFragment
,其定义如下。从下面的代码我们可以看出,上述用到的 ViewModelStore 实例就是 HolderFragment
的一个局部变量。所以,ViewModel 使用空的 Fragment 管理生命周期实锤了。
public class HolderFragment extends Fragment implements ViewModelStoreOwner {
private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();
private ViewModelStore mViewModelStore = new ViewModelStore();
public HolderFragment() {
setRetainInstance(true);
}
// ...
}
此外,我们注意到上面的 HolderFragment 的构造方法中还调用了 setRetainInstance(true)
这一行代码。我们进入该方法看它的注释:
Control whether a fragment instance is retained across Activity
re-creation (such as from a configuration change). This can only
be used with fragments not in the back stack. If set, the fragment
lifecycle will be slightly different when an activity is recreated:
就是说,当 Activity 被重建的时候该 Fragment 会被保留,然后传递给新创建的 Activity. 但是,这只适用于不处于后台的 Fragment. 所以,如果 Activity 处于后台的时候,Fragment 不会保留,那么它得到的 ViewModelStore
实例就不同了。
所以,总结下来,准确地将:当 Activity 处于前台的时候被销毁了,那么得到的 ViewModel 是之前实例过的 ViewModel;如果 Activity 处于后台时被销毁了,那么得到的 ViewModel 不是同一个。举例说,如果 Activity 因为配置发生变化而被重建了,那么当重建的时候,ViewModel 是之前的实例;如果因为长期处于后台而被销毁了,那么重建的时候,ViewModel 就不是之前的实例了。
回到之前的 holderFragmentFor()
方法,我们看下这里具体做了什么,其定义如下。
// HolderFragmentManager#holderFragmentFor()
HolderFragment holderFragmentFor(FragmentActivity activity) {
// 使用 FragmentManager 获取 HolderFragment
FragmentManager fm = activity.getSupportFragmentManager();
HolderFragment holder = findHolderFragment(fm);
if (holder != null) {
return holder;
}
// 从哈希表中获取 HolderFragment
holder = mNotCommittedActivityHolders.get(activity);
if (holder != null) {
return holder;
}
if (!mActivityCallbacksIsAdded) {
mActivityCallbacksIsAdded = true;
activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
}
holder = createHolderFragment(fm);
// 将新的实例放进哈希表中
mNotCommittedActivityHolders.put(activity, holder);
return holder;
}
首先,尝试使用 FragmentManager
来获取 HolderFragment
,如果获取不到就从 mNotCommittedActivityHolders
中进行获取。这里的 mNotCommittedActivityHolders
是一个哈希表,每次实例化的新的 HolderFragment 会被添加到哈希表中。
另外,上面的方法中还使用了 ActivityLifecycleCallbacks 对 Activity 的生命周期进行监听。其定义如下,
private ActivityLifecycleCallbacks mActivityCallbacks =
new EmptyActivityLifecycleCallbacks() {
@Override
public void onActivityDestroyed(Activity activity) {
HolderFragment fragment = mNotCommittedActivityHolders.remove(activity);
}
};
当 Activity 被销毁的时候会从哈希表中移除映射关系。所以,每次 Activity 被销毁的时候哈希表中的映射关系都不存在了。而之所以 ViewModel 能够实现在 Activity 配置发生变化的时候获取之前的 ViewModel 是通过上面的 setRetainInstance(true)
和 findHolderFragment(fm)
来实现的。
总结
以上就是 ViewModel 的生命周期的总结。我们只是通过对主流程的分析研究了它的生命周期的流程,实际上内部还有许多小细节,逻辑也比较简单,我们就不一一说明了。
其实,从 Google 的官方文档中,我们也能够得到上面的总结,
这里使用了 Activity rotated
,也就是 Activity 处于前台的时候配置发生变化的情况,而不是处于后台,不知道你之前有没有注意这一点呢?
以上。
(如有疑问,可以在评论中交流)
如果你喜欢这篇文章,请点赞哦!你也可以在以下平台关注我哦:
- 博客:https://shouheng88.github.io/
- 掘金:https://juejin.im/user/585555e11b69e6006c907a2a
- Github:https://github.com/Shouheng88
- CSDN:https://blog.csdn.net/github_35186068
- 微博:https://weibo.com/u/5401152113
所有的文章维护在:Github, Android-notes
获取更多技术文章可以直接关注我的公众号「Hello 开发者」,另外感兴趣的可以加入技术 QQ 交流群:1018235573.
以上,感谢阅读~