【背上Jetpack之ViewModel】即使您不使用MVVM也要了解ViewModel ——ViewModel 的职能边界

8,799 阅读10分钟

系列文章

【背上Jetpack】Jetpack 主要组件的依赖及传递关系

【背上Jetpack】AdroidX下使用Activity和Fragment的变化

【背上Jetpack之Fragment】你真的会用Fragment吗?Fragment常见问题以及androidx下Fragment的使用新姿势

【背上Jetpack之Fragment】从源码角度看 Fragment 生命周期 AndroidX Fragment1.2.2源码分析

【背上Jetpack之OnBackPressedDispatcher】Fragment 返回栈预备篇

【背上Jetpack之Fragment】从源码的角度看Fragment 返回栈 附多返回栈demo

【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析

目录

前言

Android 开发时,我们使用 activity 和 fragment 作为视图控制器, 可能还会使用有一些类可以存储和提供 UI 数据(例如MVP中的 Presenter

但是 当配置更改时(如旋转屏幕),activity 会重建,但对于 UI 数据的持有者呢?

  • 开发者需要重新保存相关的信息并传递给重建的 activity ,否则开发者必须再次获取数据(通过网络请求或本地数据库)
  • 由于 UI 数据的持有者的生命周期可能比 activity 长,因此开发者还需要避免出现内存泄漏的问题

如何解决上述问题?ViewModel

本文重点介绍 ViewModel 的职责(what)以及重点功能的实现原理(how),即使您不使用 Jetpack MVVM 架构,也要了解一下 ViewModel

ViewModel 的原理部分要求您了解 activity 的启动流程,这部分内容网上文章很多,本文不再赘述

ViewModel 的职责

我先上个 视频 ,这个小姐姐表述的比文字更形象

ViewModel 主要用于存储 UI 数据以及生命周期感知的数据

图片来自 Android Architecture Components: ViewModel

ViewModel生命周期

ViewModel 的生命周期 ,图片来自 官方文档

作为数据持有者

ViewModel 能够实时进行配置更改。 这意味着即使在手机旋转后销毁并重新创建 activity 之后,您仍然拥有相同的 ViewModel 和相同的数据。 因此:

  • 您无需担心 UI 数据持有者的生命周期。 ViewModel 将由工厂自动创建,您无需自行创建和销毁
  • 数据将始终更新,旋转手机后,您将获得与以前相同的数据。 因此,您无需手动将数据传递给新的 activity 实例或再次调用网络或数据库来获取数据。

Fragment 间共享数据

一个 activity 中的两个或更多 fragment 需要相互通信是很常见的。例如您有一个片段,用户在其中从列表中选择一个 item,另一个片段显示了所选 item 的内容。 传统做法两个 fragment 都需要定义一些接口,并且宿主 activity 必须将两者绑定在一起。 此外,两个 fragment 都必须处理另一个 fragment 尚未创建或不可见的情况。

可以通过使用 ViewModel 对象解决此问题。 这些 fragment 可以使用 activity 范围内共享一个 ViewModel 来处理此通信,如以下示例代码所示:

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}


public class MasterFragment extends Fragment {
    private SharedViewModel model;

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        model.getSelected().observe(getViewLifecycleOwner(), { item ->
           // Update the UI.
        });
    }
}

由于 两个 fragment 使用的都是 activity 范围的 ViewModelViewModelProvider 构造器传入的 activity ),因此它们获得了相同的 ViewModel 实例,自然其持有的数据也是相同的,这也 保证了数据的一致性

这种方法具有以下优点:

  • 宿主 activity 无需执行任何操作,也无需了解此通信。

  • SharedViewModel 外,fragment 不需要彼此了解。 如果其中一个 fragment 消失了,则另一个继续照常工作。

  • 每个 fragment 都有其自己的生命周期,并且不受另一个 fragment 的生命周期影响。 如果一个 fragment 替换了另一个 fragment,则 UI 可以继续正常工作而不会出现任何问题。

代替 Loader

CursorLoader 这样的 Loader 类经常用于使应用程序 UI 中的数据与数据库保持同步。您可以使用 ViewModel 和其他一些类来替换 Loader。 使用 ViewModel 可将视图控制器与数据加载操作分开,这意味着您在类之间的强引用较少。

在使用 Loader 的一种常见方法中,应用程序可能会使用 CursorLoader 来观察数据库的内容。 当数据库中的值更改时,加载程序会自动触发数据的重新加载并更新 UI

图片来自 官方文档

ViewModelRoomLiveData 一起使用以替换 Loader。 ViewModel 确保数据在设备配置更改后仍然存在。 当数据库发生更改时,Room 会通知 LiveData ,然后 LiveData 会使用修改后的数据更新 UI

图片来自 官方文档

总结

  • ViewModel 可作为 UI 数据的持有者,在 activity/fragment 重建时 ViewModel 中的数据不受影响,同时可以避免内存泄漏
  • 可以通过 ViewModel 来进行 activity 和 fragment ,fragment 和 fragment 之间的通信,无需关心通信的对方是否存在,使用 application 范围的 ViewModel 可以进行全局通信
  • 可以代替 Loader

ViewModel 源码分析

分析源码时我们可以不计较细枝末节,只分析主要的逻辑即可。因此我们来思考几个问题,并从源码中寻找答案

  • 如何做到 activity 重建后 ViewModel 仍然存在?

  • 如何做到 fragment 重建后 ViewModel 仍然存在?

  • 如何控制作用域?(即保证相同作用域获取的 ViewModel 实例相同)

  • 如何避免内存泄漏?

维持我们一贯的风格,我们先来大胆地猜一猜

对于问题1 :activity 有着 saveInstanceState 机制,因此可能通过该机制来处理(事实证明不是

对于问题2:可能 fragment 通过 宿主 activity 或 父 fragment 的帮助来确保 ViewModel 实例在重建后仍然存在

对于问题3:实现一个类似单例的效果,相同作用域获取的对象是相同的

对于问题4:避免 ViewModel 持有 view 或 context 的引用

首先我们要先了解一下 ViewModel 的结构

  • ViewModel:抽象类,主要有 clear 方法,它是 final 级,不可修改,clear 方法中包含 onClear 钩子,开发者可重写 onClear 方法来自定义数据的清空

  • ViewModelStore:内部维护一个 HashMap 以管理 ViewModel

  • ViewModelStoreOwner:接口,ViewModelStore 的作用域,实现类为 ComponentActivityFragment,此外还有 FragmentActivity.HostCallbacks

  • ViewModelProvider:用于创建 ViewModel,其构造方法有两个参数,第一个参数传入 ViewModelStoreOwner ,确定了 ViewModelStore 的作用域,第二个参数为 ViewModelProvider.Factory,用于初始化 ViewModel 对象,默认为 getDefaultViewModelProviderFactory() 方法获取的 factory

简单来说 ViewModelStoreOwner 持有 ViewModelStore 持有 ViewModel

1. 如何做到 activity 重建后 ViewModel 仍然存在?

【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析 中我们提到了 androidx.core.app.ComponentActivity 的引入并探讨了其作为中间层的作用

我们已经讲过 SavedStateRegistryOwnerOnBackPressedDispatcherOwner 这两种角色,而今天我们来聊一下

ViewModelStoreOwnerHasDefaultViewModelProviderFactory 。其中前者代表着 ViewModelStore 的作用域,后者来标记 ViewModelStoreOwner 拥有默认的 ViewModelProvider.Factory

那么 ViewModel 的逻辑肯定就在该类了

ComponentActivity 实现了 ViewModelStoreOwner 接口,意味着需要重写 getViewModelStore() 方法,该方法为 ComponentActivitymViewModelStore 变量赋值。activity 重建后 ViewModel 仍然存在,只要保证 activity 重建后 mViewModelStore 变量值不变即可

顺着这个思路,我们来看一下 getViewModelStore() 的实现

public ViewModelStore getViewModelStore() {
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            //核心,在该位置重置 mViewModelStore
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}

mViewModelStore 的值由 getLastNonConfigurationInstance() 返回的 NonConfigurationInstances 对象中的 viewModelStore 赋值,如果此时还为空才去 new ViewModelStore 对象。因此我们只需找到

getLastNonConfigurationInstance 中的 NonConfigurationInstances 在哪里保存的即可

getLastNonConfigurationInstance 为平台 activity 中的方法,返回 mLastNonConfigurationInstances.activity

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

那么我们看一下 mLastNonConfigurationInstances 的赋值位置

//省略其他参数
final void attach(NonConfigurationInstances lastNonConfigurationInstances){
	mLastNonConfigurationInstances = lastNonConfigurationInstances;
    //...
}

了解过 activity 的启动流程的小伙伴肯定知道,这个 attach 方法是 ActivityThread 中的 performLaunchActivity 调用的

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    //省略其他参数
    activity.attach(r.lastNonConfigurationInstances);
    r.lastNonConfigurationInstances = null;
    //...
}

深入追踪源码我们整理一下调用流程

由于 ActivityThread 中的 ActivityClientRecord 不受 activity 重建的影响,所以 activity 重建时 mLastNonConfigurationInstances 能够得到上一次的值,使得 ViewModelStore 值不变 ,问题1就解决了

2. 如何做到 fragment 重建后 ViewModel 仍然存在?

对于问题2,有了上面的思路我们可以认定 fragment 重建后其内部的 getViewModelStore() 方法返回的对象是相同的。

// Fragment.java
public ViewModelStore getViewModelStore() {
    return mFragmentManager.getViewModelStore(this);
}

可以看到 getViewModelStore() 内部调用的是 mFragmentManager(普通fragment 对应 activity 中的 FragmentManager,子 fragment 则对应父 fragment 的 childFragmentManager)的 getViewModelStore() 方法

// FragmentManager.java
private FragmentManagerViewModel mNonConfig;

ViewModelStore getViewModelStore(@NonNull Fragment f) {
    return mNonConfig.getViewModelStore(f);
}

而 FragmentManager 中的 getViewModelStore 使用的是 mNonConfig ,mNonConfig 竟然是个 ViewModel!

// FragmentManagerViewModel.java
private final HashMap<String, FragmentManagerViewModel> mChildNonConfigs = new HashMap<>();
private final HashMap<String, ViewModelStore> mViewModelStores = new HashMap<>();

FragmentManagerViewModel 管理着内部的 ViewModelStore 和 child 的 FragmentManagerViewModel 。因此保证 mNonConfig 值不变即能确保 fragment 中的 getViewModelStore() 不变。那么看看 mNonConfig 赋值的位置

// FragmentManager.java
void attachController(@NonNull FragmentHostCallback<?> host, @NonNull FragmentContainer container, @Nullable final Fragment parent) {
    //...
    if (parent != null) {
        // 嵌套 fragment 的情况,有父 fragment
        mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
    } else if (host instanceof ViewModelStoreOwner) {
        // host 是 FragmentActivity.HostCallbacks
        ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
        mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
    } else {
        mNonConfig = new FragmentManagerViewModel(false);
    }
}


// FragmentManagerViewModel.java
static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) {
    ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore,
            FACTORY);
    return viewModelProvider.get(FragmentManagerViewModel.class);
}

我们先看 fragment 的直接宿主是 activity (即没有嵌套)的情况,mNonConfig 由FragmentManagerViewModel.getInstance(viewModelStore) 赋值,而 getInstance 中使用的是 ViewModelProvider 获取 ViewModel ,根据我们上面的分析,只要保证作用域(viewModelStore)相同,即可获取相同的 ViewModel 实例,因此我们需要看一下 host 的 getViewModelStore 方法。经过一番寻找,host 是 FragmentActivity.HostCallbacks

// FragmentActivity.java 内部类
class HostCallbacks extends FragmentHostCallback<FragmentActivity> implements ViewModelStoreOwner, OnBackPressedDispatcherOwner {
    public ViewModelStore getViewModelStore() {
        // 宿主 activity 的 getViewModelStore
    	return FragmentActivity.this.getViewModelStore();
	}
}

host 的 getViewModelStore 方法返回的是宿主 activity 的 getViewModelStore() ,而 activity 重建后其内部的 mViewModelStore 是不变的,因此即使 activity 重建,其内部的 FragmentManager 对象变化,但 FragmentManager 内部的 FragmentManagerViewModel 的实例(mNonConfig)不变,mNonConfig.getViewModelStore 不变,fragment 的 getViewModelStore() 亦不变,fragment 重建后其内部的 ViewModel 仍然存在

对于嵌套 fragment ,mNonConfig 通过 parent.mFragmentManager.getChildNonConfig(parent) 获取

// FragmentManager.java
private FragmentManagerViewModel getChildNonConfig(@NonNull Fragment f) {
    return mNonConfig.getChildNonConfig(f);
}

上文提到 FragmentManagerViewModel 管理着 mChildNonConfigs Map,因此子 fragment 重置后其内部的 mNonConfig 对象也是相同的

至此问题 2 就解决了

3. 如何控制作用域?

对于问题3,我们知道 ViewModelStoreOwner 代表着作用域,其内部唯一的方法返回 ViewModelStore 对象,也即不同的作用域对应不同的 ViewModelStore ,而 ViewModelStore 内部维护着 ViewModel 的 HashMap ,因此只要保证相同作用域的 ViewModelStore 对象相同就能保证相同作用域获取到相同的 ViewModel 对象,而问题1我们已经解释了重建时如何保证 ViewModelStore 对象不变。

因此问题3也解决了。

4. 如何避免内存泄漏?

对于问题4,由于 ViewModel 的设计,使得 activity/fragment 依赖它,而 ViewModel 不依赖 activity/fragment。因此只要不让 ViewModel 持有 context 或 view 的引用,就不会造成内存泄漏

总结

简单的总结一下:

  • activity 重建后 mViewModelStore 通过 ActivityThread 的一系列方法能够保持不变,从而当 activity 重建时 ViewModel 中的数据不受影响

  • 通过宿主 activity 范围内共享的 FragmentManagerViewModel 来存储 fragment 的 ViewModelStore 和子 fragment 的 FragmentManagerViewModel ,而 activity 重建后 FragmentManagerViewModel 中的数据不受影响,因此 fragment 内部的 ViewModel 的数据也不受影响

  • 通过同一 ViewModelStoreOwner 获取的 ViewModelStore 相同,从而保证同一作用域通过 ViewModelProvider 获取的 ViewModel 对象是相同的

  • 通过单向依赖(视图控制器持有 ViewModel )来解决内存泄漏的问题

ViewModel 和 onSaveInstanceState

ViewModelonSaveInstanceState 的功能有些类似,但它们也有很多差异

从存储位置上来说,ViewModel 是在内存中,因此其读写速度更快,但当进程被系统杀死后,ViewModel 中的数据也不存在了。从数据存储的类型上来看,ViewModel 适合存储相对较重的数据,例如网络请求到的 list 数据,而 onSaveInstanceState 适合存储轻量可序列化的数据

那么我们该如何使用呢?可以使用 viewmodel-savedstate 库,详情参考 【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析

关于我

我是 Fly_with24