漫谈 MVVM(1)ViewModel_DataBinding 核心原理 (2)
<img src="漫谈 MVVM.assets/image-20200530155155198.png" alt="image-20200530155155198" style="zoom:67%;" />
既然是探索 ViewModel 的本源,那就从它的官方注解开始吧。
 
 这段话的大意是:
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(或者你可以自定义观察者模式框架),用他们来暴露 ViewModel 给 V 层 
核心功能
ViewModel 的核心功能:在适当的时机执行回收动作,也就是 onCleared() 函数释放资源。而这个合适的时机,可以理解为 Activity 销毁,或者 Fragment 解绑。
借用一张图来解释,就是:
 
 在整个 Activity 还处于存活状态时,ViewModel 都会存在。而当 Activity 被 finish 的时候,ViewModel 的 onCleared 函数将会被执行,我们可以自己定义函数内容,清理我们自己的资源,在 Activity 被销毁之后。该 ViewModel 也不再被任何对象持有,下次 GC 时它将被 GC 回收。
基本用法
创建一个新的项目,定义我们自己的 UserModel 类,继承 ViewModel:
import android.util.Logimport androidx.lifecycle.ViewModel
class 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.ViewModelProvider
class 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()}}
就这么简单,运行程序能看到日志:
<img src="漫谈 MVVM.assets/image-20200602104022542.png" alt="image-20200602104022542" style="zoom: 100%;" />
同时 ViewModelProvider 也支持两个参数的构造函数,除了上面的 owner=this 之外,还可以传入另一个 Factory 参数。
如果不传入这个 Factory,源码中会在拿到 ViewModel 的 class 对象之后通过无参构造函数进行反射创建对象。但是如果 ViewModel 要用有参构造函数来创建的话,那就必须借助 Factory:
// ViewModelclass UserModel(i: Int, s: String) : ViewModel() {
var i: Int = ivar 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 = s")}
}
// ViewModelFactoryclass UserModelFactory(val i: Int, val s: String) : ViewModelProvider.Factory {override fun <T : ViewModel?> create(modelClass: Class<T>): 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 的构造函数:
 
 上面标重点的注释的意思是:创建一个 ViewModelProvider,这将会创建 ViewModel 并且把他们保存到给定的 owner 所在的仓库中。这个函数最终调用了重载的构造函数:
 
 这个构造函数有两个参数,一个 store,是刚才通过 owner 拿到的,一个是,Factory。store 顾名思义,是用来存储 ViewModel 对象的,而 Factory 的意义,是为了通过 class 反射创建对象做准备的。
使用构造函数创建出一个 ViewModelProvider 对象之后,再去get(UserModel::class.java)
 
 通过一个 class 对象,拿到他的 canonicalName 全类名。然后调用重载 get 方法来获取真实的 ViewModel 对象。
 
 这个 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 onCreate(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。











 
    
评论