漫谈 MVVM(1)ViewModel_DataBinding 核心原理,kotlin 开发安卓游戏
这个 get 函数有两个参数,其一,key,字符串类型。用于做标记,使用的是一个定死的字符串常量 DEFAULT_KEY 拼接上 modelClass 的全类名,其二,modelClass 的 class 对象,内部代码会使用 class 进行反射,最终创建出 ViewModel 对象。
上面提到了一个重点:Store 仓库,创建出来的 ViewModel 都会被存入 owner 所在的仓库。那么,阅读仓库的源码:
那么一个 Activity,它作为 ViewModelStoreOwner,他自己的 viewModelStore 何时清理?
答案是:onDestroy() . 但是这里有一个特例,配置改变,比如屏幕旋转时,ViewModelStore 并不会被清理。并且,Fragment 的源码中也有类似的调用:
总结
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,使用**<layout></layout>**标签包裹原来的布局,并且插入<data>节点
<?xml version="1.0" encoding="utf-8"?><layout><data><import type="androidx.databinding.ObservableMap" /><import type="androidx.databinding.ObservableList" />
<variablename="userBean"type="com.zhou.databinding.UserBean" /><variablename="map"type="ObservableMap<String, Object>" />
<LinearLayout 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">
这里支持 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: ActivityMainBindingoverride fun onCreat
e(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = DataBindingUtil.setContentView(this, R.layout.activity_main)binding.lifecycleOwner = thisbinding.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 = "asfdsafi"
Thread.sleep(10)}}.start()
}}
上面的代码,如果运行起来,
可以看到我并未主动去使用 textview 的引用去操控它的 text 属性。这些工作都是在 databinding 框架中完成的。至于更具体更复杂的用法,本文不再赘述。网上很多骚操作。
核心原理
核心功能是 数据绑定,也就是说,只要知道了 databinding 是如何在数据变化时,通知到 view 让它改变属性的,databinding 的秘密就算揭开。直接从代码进入源码。这一切的源头,都是由于我们使用了 DataBindingUtil 来进行绑定引起的。那么就从它开始。
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)binding.title = "asfdsaf"
注释的大意是:将 Activity 的内容 View 设置给 指定 layout 布局,并且返回一个关联之后的 binding 对象。指定的 layout 资源文件不能是 merge 布局。
随后该函数调用到了:
这里首先使用 activity.setContentView,将 layoutId 设置进去,常规操作。然后,拿到 activity 的 decorView,进而拿到 contentView,随后调用 bindToAddViews。
继续追踪 bind 函数:
目标转移到了 sMapper.getDataBinder(),进去看了之后发现是抽象类,找到他的实现类:
结果发现了我自己的包名,看到这里应该有些明白了,我并没有写这个 DataBindingMapperImpl 类,它只能是 as 帮我们自动生成的。
所以,绑定 View 和数据的具体代码应该在这个类里面有答案,经过追踪,发现代码走到了这一行:
回到一开始,
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
这句话,饶了一大圈,最终得到了一个 ActivityMainBindingImpl 对象,随后我们用这个对象去操作 view 引用来绑定数据
binding.title = "asfdsaf"
那就直接从这个 title 看起,上面是 kotlin 的 setTitle 写法,直接看 setTitle 方法:
其实它就是把 xml 布局文件中的 title 属性值设置为 传入的形参值。然后 notifyPropertyChanged(BR.title): 通知 ID 为 BR.title 的属性值发生了改变。
直接进到了观察者模式 Observable 接口的一个实现类 BaseObservable,由于 as 的原因,代码无法继续索引(它会直接跳到 xml 文件),但是经过 debug,我发现,当 title 发生变化时,
从上面的命名可以看出,DataBinding 框架应该是给每一个 xml 中定义的变量 variable 都建立了一个独立的监听器,在 variable 发生变化时,这个监听器会在 variable 发生改变时,通知界面元素发生属性变更。,查找这个监听器的调用位置 executeBinding()函数,结果有了意外发现,“双向数据绑定”的原理也被揭开。
这里传了 3 个参数,BeforeTextChanged,OnTextChanged,AfterTextChanged,刚好对应了 TextWatcher 接口中的 3 个方法。进入看一眼上面的 setTextWatcher():
在 textView 的内容发生变更的时候,也会执行到监听器的 onChange 函数,进行数据变更。
上面的追踪仅仅是针对
<variablename="title"type="java.lang.String" />
这一种定义 databinding 变量的方式。如果是 map 类型,map 的内部元素发生变更,UI 也是可以随之更新的,又是怎么回事呢?
<variablename="map"type="ObservableMap<String, Object>" />
经过一番追踪,发现有点不同。map 的用法有点不同:
<TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@{String.valueOf(map.count)}"android:textSize="30sp"tools:text="userBean:" />
当我在 activity 中变更 map 的元素值的时候,
会执行到 setMap 方法:
当 map 的某一个元素值发生变化时,会执行到 handlerFieldChange,
随后 onFieldChange 函数,
如果确认发生变更,就会 requestRebind()重新去绑定最新的 map 对象。
补充说明一点:
binding.lifecycleOwner = this
这句代码,
如果一个 DataBinding 对象的 mLifeCycleOwner 不是空,那么:
在绑定数据的时候,就会去判定当前 mLifeCycleOwner 是不是 STARTED 之后,如果不是,数据的绑定都不会执行。
总结
综合以上所有小结论,总结一下:
在 layout 中定义的每一个被使用的 variable 都会建立独立的监听器,没有被用到的,并不会有监听
数据的单项绑定:数据变化,通知 View 设置属性,是通过观察者模式设置监听器来实现,具体的监听方式和 variable 的类型有关
数据的双向绑定:其实是通过 textWatch 接口来实现,当 view 的 text 属性发生改变,会通知到该 view 已经绑定的监听器更新数据
DataBinding 提供了很多可观察的类型, 如 ObservableMap,ObservableList 等,如果是集合类型,必须用这个,才能实现内部元素的变动对接 UI 的变动。自定义类型可以继承 BaseObservable 并且重写 get set 方法来实现同样的绑定效果(具体如何做,这里不赘述)。
DataBinding 的 mLifeCycleOwner 属性可以让数据绑定感应 Activity/Fragment 的生命周期,防止内存泄漏.
未完待续....
评论