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 站视频讲解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348