c# mvvm模式获取当前窗口_ViewModel_DataBinding核心原理【漫谈MVVM】

6e43c8575d7f42b6430c7394b3abcf74.gif

前言

在开发模式的演进过程中 MVC,MVP,MVVM一一登上舞台。但是并不意味着MVVM一定就比MVC MVP优秀。不同的项目有不同的体量,开发中要根据项目体量选择合适的开发模式。

市面上介绍mvvm的项目不在少数,但是看了很多,都在介绍源码原理,开发中的踩坑过程,而且有的是过时的资料,却很少见到能够直接从项目需求入手帮助不熟悉MVVM的开发者从入门到熟悉原理,再到框架优化的。这个空缺我来补充,如果提及到一些其他原理性的东西不便长篇展示,我会引用链接,或者引用他文的原文加以说明。

特别说明: 以下所有信息都基于截止到 2020年6月2日10点23分的最新官方资料和源码版本。如果存在任何历史版本的差异,本文不会过分纠结。

正文大纲

  • 有关MVVM的几个重要组件
  • ViewModel
  • DataBinding
  • LiveData
  • LiveDataBus
  • 案例实操
  • MVVM的优缺点

正文

有关MVVM的几个重要组件

ViewModel

ViewModel 是androidx包中抽象类。它是谷歌开放给全世界开发者用来改善项目架构的一个组件。

b6a7cd2d7345a25a947ee8fb78b2ad27.png

既然是探索ViewModel的本源,那就从它的官方注解开始吧。

ad0fc76e8f66f1312faac8991d948a04.png

这段话的大意是:
ViewModel是一个准备和管理Activity和Fragment的数据的类。它也可以掌控Activity、Fragment和应用中其他部分的通讯。 一个ViewModel总是关联到一个域(Activity或Fragment)被创建。并且只要域是存活的,ViewModel就会一直被保留。比如。如果域是一个Activity,ViewModel就会存活,直到Activity被finish。 换句话说,这意味着,如果它的持有者由于配置改变而被销毁时(比如屏幕旋转),ViewModel并不会被销毁。新的持有者实例,将会仅仅重新连接到已经存在的ViewModel。 ViewMode存在的目的,就是为Activity/Fragment 获得以及保留 必要信息。Activity / Fragment 应该可以观察到VIewModel的变化,ViewModel通常通过LiveData或者DataBinding 暴露信息。你也可以你自己喜欢的使用可观察的结构框架。 ViewModel仅有的职责,就是为UI管理数据,它不应该访问到你任何的View层级 或者 持有Activity 、Fragment的引用。
谷歌爸爸其实已经把意思讲的很明白,上面一段话中有几个重点:
  • ViewModel 唯一的职责 就是 在内存中保留数据

  • 多个Activity或者Fragment可以共用一个ViewModel

  • 在屏幕旋转时,ViewModel 不会被重建,而只会连接到重新创建的Fragment/Activity

  • 使用ViewModel有两种方式,LiveData或者

    DataBinding(或者你可以自定义观察者模式框架),用他们来暴露ViewModelV

核心功能

ViewModel的核心功能:在适当的时机执行回收动作,也就是 onCleared() 函数释放资源。而这个合适的时机,可以理解为 Activity销毁,或者Fragment解绑。

借用一张图来解释,就是:

46ea22a2141939c9ac238dcfbb78f757.png

在整个Activity还处于存活状态时,ViewModel都会存在。而当Activity被finish的时候,ViewModel的onCleared函数将会被执行,我们可以自己定义函数内容,清理我们自己的资源,在Activity被销毁之后。该ViewModel也不再被任何对象持有,下次GC时它将被GC回收。

基本用法

创建一个新的项目,定义我们自己的UserModel类,继承ViewModel:
import android.util.Logimport androidx.lifecycle.ViewModelclass UserModel : ViewModel() {   init {       Log.d("hankTag", "执行ViewModel必要的初始化")   }   override fun onCleared() {       super.onCleared()       Log.d("hankTag", "执行ViewModel 清理资源")   }   fun doAction() {       Log.d("hankTag", "执行ViewModel doAction")   }} 在View层使用定义好的ViewModel:
import androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport androidx.lifecycle.ViewModelProviderclass MainActivity : AppCompatActivity() {   override fun onCreate(savedInstanceState: Bundle?) {       super.onCreate(savedInstanceState)       setContentView(R.layout.activity_main)       // 获取ViewModel对象       val userModel = ViewModelProvider(this).get(UserModel::class.java)       // 使用ViewModel对象的函数       userModel.doAction()   }}

就这么简单,运行程序能看到日志:

945d11b233c0daec4df8603f7d0ea06a.png

同时ViewModelProvider也支持两个参数的构造函数,除了上面的owner=this之外,还可以传入另一个Factory参数。

如果不传入这个Factory,源码中会在拿到ViewModel的class对象之后通过无参构造函数进行反射创建对象。但是如果ViewModel要用有参构造函数来创建的话,那就必须借助Factory:
// ViewModelclass UserModel(i: Int, s: String) : ViewModel() {   var i: Int = i   var s: String = s   init {       Log.d("hankTag", "执行ViewModel必要的初始化")   }   override fun onCleared() {       super.onCleared()       Log.d("hankTag", "执行ViewModel 清理资源")   }   fun doAction() {       Log.d("hankTag", "执行ViewModel doAction: i = $i, s : $s")   }}// ViewModelFactoryclass UserModelFactory(val i: Int, val s: String) : ViewModelProvider.Factory {   override fun ViewModel?> create(modelClass: Class): T {       return modelClass.getConstructor(Int::class.java, String::class.java).newInstance(i, s)   }}// View层class MainActivity : AppCompatActivity() {   override fun onCreate(savedInstanceState: Bundle?) {       super.onCreate(savedInstanceState)       setContentView(R.layout.activity_main)       // 获取ViewModel对象       val userModel = ViewModelProvider(this, UserModelFactory(1, "s")).get(UserModel::class.java)       // 使用ViewModel对象的函数       userModel.doAction()   }} 运行结果:
06-02 11:20:53.196 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel必要的初始化06-02 11:20:53.196 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel doAction: i = 1, s : s06-02 11:20:57.836 32569-32569/com.zhou.viewmodeldemo D/hankTag: 执行ViewModel 清理资源

核心原理

源码探索的目标是,ViewModel是如何感知Activity的生命周期清理自身资源的。其实也就是看 onCleared函数是如何被调用的。
  • ViewModelProvider(this).get(UserModel::class.java)上面这句代码是是用来获得ViewModel对象的。这里分为2个部分,其一,ViewModelProvider的构造函数:61c0f0a019215b934667546237125d13.png上面标重点的注释的意思是:创建一个ViewModelProvider,这将会创建ViewModel并且把他们保存到给定的owner所在的仓库中。这个函数最终调用了重载的构造函数:eaabca9e98d0df8141ad7f497d6a832c.png这个构造函数有两个参数,一个store,是刚才通过owner拿到的,一个是,Factory。store顾名思义,是用来存储ViewModel对象的,而Factory的意义,是为了通过class反射创建对象做准备的。使用构造函数创建出一个ViewModelProvider对象之后,再去 get(UserModel::class.java)ce3363e464d28af24155718502d0dfb4.png通过一个class对象,拿到他的canonicalName全类名。然后调用重载get方法来获取真实的ViewModel对象。4a66c2d309b2525ac02ea4a9c61e3a69.png这个get函数有两个参数,其一,key,字符串类型。用于做标记,使用的是一个定死的字符串常量DEFAULT_KEY拼接上modelClass的全类名,其二,modelClass的class对象,内部代码会使用class进行反射,最终创建出ViewModel对象。上面提到了一个重点:Store仓库,创建出来的ViewModel都会被存入owner所在的仓库。那么,阅读仓库的源码:d27c086a5e55e16a9faa1624a2c99574.png那么一个Activity,它作为ViewModelStoreOwner,他自己的viewModelStore何时清理6729da1443c2b596a25ff7689c49e72d.png答案是:onDestroy() . 但是这里有一个特例,配置改变,比如屏幕旋转时,ViewModelStore并不会被清理。并且,Fragment的源码中也有类似的调用:4c1bc03c89cf8b3015a656db9b1f3218.png

总结

ViewModel的核心,是自动清理资源。我们可以重写onCleared函数,这个函数将会被ViewModel所在的Activity/Fragment 执行onDestory的时候被调用,但是当屏幕旋转的时候,并不会清理。在ViewModel的架构中,有几个关键类,
  • ViewModelProvider 用于获取ViewModel

  • ViewModelStore 用于存储ViewModel

  • ViewModelStoreOwner 用于提供ViewModelStore对象,Activity和Fragment都是ViewModelStoreOwner 的实现

  • ViewModelProvider的内部类Factory,用于支持ViewModel的有参构造函数,毕竟ViewModel对象是通过class反射创建出来的,需要支持默认无参,以及手动定义有参构造函数

DataBinding

DataBinding,单词意思: 数据绑定,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。MVVM 相对于 MVP,其实就是将 Presenter 层替换成了 ViewModel层。DataBinding 能够省去我们一直以来的findViewById() 步骤,大量减少Activity 内的代码,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常. DataBinding
  • 支持在java代码中不用findViewById来获取控件,而直接通过DataBinding对象的引用即可拿到所有的控件id
  • 进行数据绑定,使得ViewModel(数据)变化时,View控件的属性随之改变
  • 支持 数据的双向绑定,改变View控件的属性,那该属性绑定的ViewModel(数据)随之改变
  • 支持将任何类型的ViewModel绑定到View控件上,包括系统提供的类型(包括基础类型集合类型),以及自定义的类型
  • 支持特殊的View属性,比如ImageView的图片源,可以自定义图片加载的具体过程(Glide...)
  • 支持在xml中写简单的表达式, 比如函数调用,三元表达式...
  • 支持对res资源文件的引用,比如 dimen,string...
  • 支持与 LiveData(下文会解释概念)合作,让数据的变动 关联 Activity / Fragment 的 生命周期
ViewModel 的注释中我们得知,DataBinding是向View层暴露ViewModel的一种方式。但是事实上并非如此,DataBinding只是数据绑定,它和ViewModel抽象类没有半毛钱关系。DataBinding绑定的双方:是 数据( 别多想,就是纯粹的数据,不涉及到生命周期) 和 视图。而MVVM的核心是ViewModel抽象类,核心功能是感知持有者Activity/Fragment的生命周期来释放资源,防止泄露。我们使用DataBinding,创建封装数据类型,也不用继承ViewModel抽象类。至于ViewModel抽象类的注释上为什么这么说,我也是很费解。但是看了许多DataBinding的资料,项目,包括在自己的项目中使用DataBinding之后,它给我的感受就是:很糟糕。没错,糟透了,也许是因为时代进步了,也许是因为我的代码洁癖,DataBinding放入我的代码,我总感觉有一种黏乎乎的感觉,就和最早的JSP一样,一个HTML文件中,混入了HTML标签,js代码,以及 java代码,尽管我承认DataBinding的功能很强大,但是使用起来确实不舒服。有一些老代码如果大量使用了这种写法,我们了解一些DataBinding核心原理也是有必要的。

核心功能

DataBinding的核心功能是:支持View和数据的单向或者双向绑定关系,并且最新版源码支持 setLifecycleOwner 设置生命周期持有者

基本用法

在所在module的build.gradle文件中,找到androd节点:插入以下代码来开启DataBinding
dataBinding{   enabled true} 改造布局xml,使用标签包裹原来的布局,并且插入 节点
<?xml version="1.0" encoding="utf-8"?>           type="androidx.databinding.ObservableMap" />        type="androidx.databinding.ObservableList" />                  name="userBean"           type="com.zhou.databinding.UserBean" />                  name="map"           type="ObservableMap, Object>" />                  name="title"           type="java.lang.String" />       xmlns:android="http://schemas.android.com/apk/res/android"       xmlns:tools="http://schemas.android.com/tools"       android:layout_width="match_parent"       android:layout_height="match_parent"       android:orientation="vertical"       tools:context=".MainActivity">                  android:id="@+id/tvTitle"           android:layout_width="wrap_content"           android:layout_height="wrap_content"           android:text="@={title}"           android:textSize="30sp"           tools:text="title:" />                  android:id="@+id/tvTitle2"           android:layout_width="wrap_content"           android:layout_height="wrap_content"           android:text="@{title}"           android:textSize="30sp"           tools:text="title:" />        <...>    这里支持import操作,类似java的import导包,导入之后就能在@{} 中使用引入之后的函数和类. 如果想双向绑定,就使用@={}。Varilable标签是用来定义数据的,name随意,字符串即可。type必须是封装类型的全类名,支持泛型实例。 Java/kotlin 代码层面: 数据的绑定支持几乎所有类型,包括jdk,sdk提供的类,或者可以自定义类:
class UserModel  {   val user = User("hank001", "man")} 对,这里命名为UserModel,但是它和androidX里面的抽象类ViewModel没有半毛钱关系。 在Activity中,需要使用DataBindingUtil将当前activity与布局文件绑定。
class MainActivity : AppCompatActivity() {   private lateinit var binding: ActivityMainBinding   override fun onCreate(savedInstanceState: Bundle?) {       super.onCreate(savedInstanceState)       binding = DataBindingUtil.setContentView(this, R.layout.activity_main)       binding.lifecycleOwner = this       binding.title = "asfdsaf"       val map = ObservableArrayMap<String, Any>().apply { put("count", 0) }       binding.map = map       binding.userBean = UserBean()       Thread {           for (i in 0..100) {               binding.title = "asfdsaf$i" // 数据变更时,UI会发生变更               map["count"] = "count$i"               Thread.sleep(10)           }       }.start()   }} 上面的代码,如果运行起来,830c9018fc6675157adbbfb83fce5c50.gif可以看到我并未主动去使用textview的引用去操控它的text属性。这些工作都是在databinding框架中完成的。至于更具体更复杂的用法,本文不再赘述。网上很多骚操作。

核心原理

核心功能是 数据绑定,也就是说,只要知道了databinding是如何在数据变化时,通知到view让它改变属性的,databinding的秘密就算揭开。直接从代码进入源码。这一切的源头,都是由于我们使用了DataBindingUtil来进行绑定引起的。那么就从它开始。
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)binding.title = "asfdsaf"

157ac703f8f55b6e241aa590ad1c52ba.png

注释的大意是:将Activity的内容View设置给 指定layout布局,并且返回一个关联之后的binding对象。指定的layout资源文件不能是merge布局。

随后该函数调用到了:

b8f06b0534daa7ffe19a9e0845d3584c.png

这里首先使用 activity.setContentView,将layoutId设置进去,常规操作。然后,拿到activitydecorView,进而拿到contentView,随后调用bindToAddViews49861dea8a5a02c9488aeffcefddde21.png继续追踪bind函数:265f1fd458c6e0788591cb9535a0fa12.png目标转移到了sMapper.getDataBinder(),进去看了之后发现是抽象类,找到他的实现类:c87df31539e604959fa8853a1d4ddb38.png结果发现了我自己的包名,看到这里应该有些明白了,我并没有写这个DataBindingMapperImpl类,它只能是as帮我们自动生成的。c87df31539e604959fa8853a1d4ddb38.png所以,绑定View和数据的具体代码应该在这个类里面有答案,经过追踪,发现代码走到了这一行:cc55867287103334a5cc04ce499bab8e.png回到一开始,
binding = DataBindingUtil.setContentView(this, R.layout.activity_main) 这句话,饶了一大圈,最终得到了一个ActivityMainBindingImpl对象,随后我们用这个对象去操作view引用来绑定数据
binding.title = "asfdsaf" 那就直接从这个title看起,上面是kotlin的setTitle写法,直接看setTitle方法:0d5370fc83901eb2256956b9d2a624a0.png其实它就是把xml布局文件中的 title属性值设置为 传入的形参值。然后 notifyPropertyChanged(BR.title): 通知 ID为 BR.title的属性值发生了改变。7d6bcf08afc9e6ec32e3eca34fb5a09b.png直接进到了观察者模式Observable接口的一个实现类BaseObservable,由于as的原因,代码无法继续索引(它会直接跳到xml文件),但是经过debug,我发现,当title发生变化时,d75e9995c2fd05d06abd1f37a62dc806.png从上面的命名可以看出,DataBinding框架应该是给每一个xml中定义的变量variable都建立了一个独立的监听器,在variable发生变化时,这个监听器会在 variable 发生改变时,通知界面元素发生属性变更。,查找这个监听器的调用位置 executeBinding()函数,结果有了意外发现,“双向数据绑定”的原理也被揭开。7d46416c73aee24e4634ff1750768e1b.png这里传了3个参数,BeforeTextChanged,OnTextChanged,AfterTextChanged,刚好对应了TextWatcher接口中的3个方法。进入看一眼上面的setTextWatcher():

64f8871a42c2cf1d1abf7de269b5c841.png

在 textView的内容发生变更的时候,也会执行到监听器的onChange函数,进行数据变更。 上面的追踪仅仅是针对
           name="title"           type="java.lang.String" /> 这一种定义databinding变量的方式。如果是map类型,map的内部元素发生变更,UI也是可以随之更新的,又是怎么回事呢?
                  name="map"           type="ObservableMap, Object>" /> 经过一番追踪,发现有点不同。map的用法有点不同:
   android:layout_width="wrap_content"   android:layout_height="wrap_content"   android:text="@{String.valueOf(map.count)}"   android:textSize="30sp"   tools:text="userBean:" /> 当我在activity中变更map的元素值的时候,7c6db9fe585ecc6a52cb5a784145595c.png会执行到setMap方法:36bc8cb1ce24207050b8a9374ab7b3cf.pngmap的某一个元素值发生变化时,会执行到handlerFieldChangeb1bd090f934a2a4dc41f434068d2a50d.png随后 onFieldChange函数,f7f0fdece77ba48d8371eaf8772597f6.png如果确认发生变更,就会requestRebind()重新去绑定最新的map对象。 补充说明一点:
binding.lifecycleOwner = this 这句代码,b315668f45ebd307b0cfaf550dcb2eb5.png如果一个DataBinding对象的mLifeCycleOwner不是空,那么:7b686b8b14dfc584e9e76414816f7b97.png在绑定数据的时候,就会去判定当前mLifeCycleOwner是不是STARTED之后,如果不是,数据的绑定都不会执行

总结

综合以上所有小结论,总结一下:
  • 在layout中定义的每一个被使用的variable都会建立独立的监听器,没有被用到的,并不会有监听
  • 数据的单项绑定:数据变化,通知View设置属性,是通过观察者模式设置监听器来实现,具体的监听方式和variable的类型有关
  • 数据的双向绑定:其实是通过 textWatch接口来实现,当view的text属性发生改变,会通知到该view已经绑定的监听器更新数据
  • DataBinding提供了很多可观察的类型, 如 ObservableMap,ObservableList等,如果是集合类型,必须用这个,才能实现内部元素的变动对接UI的变动。自定义类型可以继承BaseObservable并且重写get set方法来实现同样的绑定效果(具体如何做,这里不赘述)。
  • DataBinding的mLifeCycleOwner属性可以让数据绑定感应Activity/Fragment的生命周期,防止内存泄漏.
未完待续....

技术直播

Live Class

1c2669de361238cb4e91b25e75ec3dee.png

6baeaf2cd0e536fefcd988a5d28ae2b5.png

↑点击直接进入课堂↑

738da18620cda68d50ff98e7b2947444.png是兄弟,就来 “kan” 90a24cff85caffd08c69a4b41443f71b.gif