ViewModel 基础使用和源码分析

前言

承接上篇的学习顺序,本文主要是对 ViewModel 的学习。ViewModel 是用来保存 UI 数据的类,并且会在配置变更后(如屏幕旋转)继续存在。先总结下 ViewModel 的特点:

  • 提供 UI 界面的数据

  • 负责和数据层通讯

  • 配置变更后继续存在

官方文档建议,我们应将应用的 UI 数据保存在 ViewModel 中,而不是 Activity 中,确保数据不会受到 Configuration Change 的影响。 尽量弱化 Activity 的职责, Activity 仅负责如何在屏幕上显示数据和接受用户互动。

如果系统销毁或重新创建 UI 控制器,存储在其中的临时数据会造成丢失。例如:在某个 Activity 中展示用户列表,因配置变更导致 Activity 重新创建。新创建的 Activity必须重新获取用户列表。对于简单的数据,可使用 onSaveInstanceState() 存储,并在 onCreate() 中通过 Bundle 进行数据恢复。但是这种方法仅适用于少量数据,不适用存储大量数据(如:用户列表和 bitmaps)。

另一个问题是,UI 控制器经常需要接受一些异步回调。UI 控制器需要管理这些异步回调,确保在界面销毁时,不会发生潜在的内存泄漏问题。这种处理方式需要耗费大量的精力,并且在配置变更重新创建对象时,重新联网获取数据也会造成资源的浪费。

ActiviyFragment 主要是用来显示 UI 数据,接受用户的交互请求或者处理系统通讯(权限请求)。如果把从数据库或网络获取的数据,都一窝蜂的堆积在 ActivityFragment 中。会造成该类代码膨胀,为日后的维护埋下了隐患。为 UI 控制器分配过多的任务,违背了单一职责原则,也会使单元测试变得困难。

将数据和 UI 分离,将会让开发和维护变得更加高效和容易。啰嗦了这么多,下面正式进入本文的主题 ViewModel

实现 ViewModel

Android Jetpack Components 提供了 ViewModel 类,用来给 UI 控制器提供数据。ViewModel 在配置变更时自动保留,以便保存的数据用于下一个 ActivityFragment 的实例。下面是一个计数器的例子,来展示 ViewModel 的数据在配置变更后继续存在的特性。

class MainViewModel : ViewModel() {

    var count = 0

}
复制代码

我们把按钮点击次数的 count 属性保存在 ViewModel 中,接下来在 Activity 中使用。

@SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val mainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        btn.setOnClickListener {
            mainViewModel.count++
            tvCount.text = "点击次数:${mainViewModel.count}"
        }
    }
复制代码

通过一个 TextView 显示按钮点击的次数,当旋转屏幕,Activity 重新创建,但 count 被没有被销毁。

如果 Activity 重新创建,它将接受到由第一个 Activity 创建的相同 MainViewModel 实例。当 Activity 关闭,framework 层会调用 ViewModelonCleared()释放资源。但需要注意的是,开发者应该自己实现 onCleared(),而不用关心 onCleared() 的调用时机。

如果你的 ViewModel 需要通过构造函数传递参数,可以使用 ViewModelFactory 来创建自定义构造函数。如:

class LoginViewModelFactory(
        private val repo: LoginDataSourceRepository
) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T =
            LoginViewModel(repo) as T
}
复制代码

注意: 不要将 Context 传入 ViewModel 中,也就是说 ViewModel 中不应当持有 ActivityFragmentView 的引用。

如果 ViewModel 需要 Applicationcontext 对象(如:使用系统服务),可以继承自 AndroidViewModel

ViewModel 的生命周期

当获取 ViewModel 时,ViewModel 对象的范围限定为传递给 ViewModelProviderLifecycleOwner 对象。当 Activity finsih 或者 Fragment detach 时,ViewModel 将会一直保留在内存中。

下图展示了 ActivityViewModel 的生命周期

viewmodel-lifecycle.png

我们应当在系统第一次调用 ActivityonCreate() 时,去获取 ViewModel 对象。当系统因配置变更时,重新创建 Activity 时,ViewModel 还是第一次获取到的实例。

Fragment 共享数据

Activity 中内嵌一个或者多个 Fragment 是常见的做法。一般情况下,Fragment 之间通信,都是采用接口回调或者 EventBus 。现在又多了一个新的选择 ViewModel,以下是官方的示例代码:

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()

    fun select(item: Item) {
        selected.value = item
    }
}

class MasterFragment : Fragment() {

    private lateinit var itemSelector: Selector

    private lateinit var model: SharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        model = activity?.run {
            ViewModelProviders.of(this).get(SharedViewModel::class.java)
        } ?: throw Exception("Invalid Activity")
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}

class DetailFragment : Fragment() {

    private lateinit var model: SharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        model = activity?.run {
            ViewModelProviders.of(this).get(SharedViewModel::class.java)
        } ?: throw Exception("Invalid Activity")
        model.selected.observe(this, Observer<Item> { item ->
            // Update the UI
        })
    }
}
复制代码

由于 MasterFragmentDetailFragment 拥有相同的宿主 Activity,因此获取到的 ViewModel 示例也是一样的。

源码解析

在分析源码前,我们先思考下面两个问题:

  • ViewModel 是通过什么存储的?

  • 系统在因配置变更,是如何保留 ViewModel 的实例?

1. ViewModelProviders

还是以 MainActivity 中的代码作为切入点。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        println("onCreate")
        setContentView(R.layout.activity_main)
        // 步骤 1
        val viewModelProvider = ViewModelProviders.of(this)
        // 步骤 2
        val mainViewModel = viewModelProvider.get(MainViewModel::class.java)
    }
    
}
复制代码

通过 of() 方法,可以获取一个 ViewModelProvider 示例。

    public static ViewModelProvider of(@NonNull FragmentActivity activity,
            @Nullable Factory factory) {
        Application application = checkApplication(activity);
        if (factory == null) {
            factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
        }
        return new ViewModelProvider(ViewModelStores.of(activity), factory);
    }
复制代码

2. ViewModelStores

ViewModelStores 主要是用来提供 ViewModelStore 的实例。通过 of() 返回一个 ViewModelStore

  public static ViewModelStore of(@NonNull FragmentActivity activity) {
        if (activity instanceof ViewModelStoreOwner) {
            return ((ViewModelStoreOwner) activity).getViewModelStore();
        }
        return holderFragmentFor(activity).getViewModelStore();
    }
复制代码

3. ViewModelStoreOwner

@SuppressWarnings("WeakerAccess")
public interface ViewModelStoreOwner {
    /**
     * Returns owned {@link ViewModelStore}
     *
     * @return a {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore();
}
复制代码

SDK 27 及以上版本,supprot 包下的 FragmentActivityFragment 实现了 ViewModelStoreOwner。该接口主要是在配置变更时,保留原有的 ViewModelStore

4. ViewModelStore

ViewModelStore 在内部通过 HashMap 来存放 ViewModel。当 ViewModelStoreOwner 因配置发生变更时,该类会被系统保留,确保新创建的 Activity 能获取到和之前一样的 ViewModelStore

ViewModelStoreOwner 被销毁,并且不会新建时。ViewModelStoreclear() 将会调用,继而调用 ViewModelonCleared() 释放资源。

到目前为止,对于提出的第一个问题。我们已经清楚了。现在让我们进入 FragmentActivity 中查看系统是如何保存 ViewModelStore 实例的。

FragmentActivity

  1. onCreate() 出发
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mFragments.attachHost(null /*parent*/);

        super.onCreate(savedInstanceState);

        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            mViewModelStore = nc.viewModelStore;
        }
       
       ...
    }
复制代码

现在可以肯定的是,ViewModelStore 对象是保存在 NonConfigurationInstances 中。getLastNonConfigurationInstance() 是定义在 Activity 中的,该方法用来获取之前 onRetainNonConfigurationInstance() 返会的 Object对象。

  * @return the object previously returned by {@link #onRetainNonConfigurationInstance()}
     */
    @Nullable
    public Object getLastNonConfigurationInstance() {
        return mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.activity : null;
    }
复制代码
  1. 现在我们看下 Activity 中的onRetainNonConfigurationInstance()
public Object onRetainNonConfigurationInstance() {
        return null;
    }
复制代码

通过注释可以发现,该方法在 Activity因配置变更并销毁的时候由系统调用,具体的调用时机是在 onStop()onDestory() 之间。

  1. 接下来我们查看 FragmentActivityonRetainNonConfigurationInstance 的具体实现。
 public final Object onRetainNonConfigurationInstance() {
        if (mStopped) {
            doReallyStop(true);
        }

        Object custom = onRetainCustomNonConfigurationInstance();

        FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

        if (fragments == null && mViewModelStore == null && custom == null) {
            return null;
        }

        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = mViewModelStore;
        nci.fragments = fragments;
        return nci;
    }
复制代码

高兴的是,我们在该方法中发现了 mViewModelStore 的身影。

现在让我们梳理下,系统对于 ViewModel 保存的逻辑。当 Activity 因配置变更销毁时,系统会调用 onRetainNonConfigurationInstance() 保存 ViewModel。在新建 Activity 中的 onCreate() 方法通过 getLastNonConfigurationInstance() 获取 NonConfigurationInstances。继而获取先前的 ViewModel 实例。

到此为止 步骤 1 中代码的执行流程就分析完了,步骤 2 的代码,我们简单看下。

    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            //noinspection unchecked
            return (T) viewModel;
        } else {
            //noinspection StatementWithEmptyBody
            if (viewModel != null) {
                // TODO: log a warning.
            }
        }

        viewModel = mFactory.create(modelClass);
        mViewModelStore.put(key, viewModel);
        //noinspection unchecked
        return (T) viewModel;
    }
复制代码

Activity 因配置变更重建时,mViewModelStore 还是一开始创建的示例,因此返回的 ViewModel 对象和最初的一样。

总结

笔者是基于 SDK 版本 27 ,Lifecycle 版本 1.1.1 分析的。需要注意的是系统在 SDK 27 之前是通过一个不可见的 Fragment ,将 setRetainInstance() 设置为 true 进行处理的。笔者不再做过多分析,感兴趣的可自行研究。如分析有误,还多请指正。

参考资料:
官方文档
B 站视频讲解

猜你喜欢

转载自juejin.im/post/5c2c0828e51d45593b4bd4c4