数据绑定分为单项绑定和双向绑定两种。单向绑定上,数据的流向是单方面的,只能从代码流向 UI;双向绑定的数据是双向的,当业务代码中的数据变化是,UI 上的数据能够得到刷新;当用户通过 UI 交互编辑了数据时,数据的变化也能自动的更新到业务代码中的数据上。
Android DataBinding Framework
在 2015 年的谷歌 IO 大会上,Android UI Toolkit 团队发布了 DataBinding 框架,将数据绑定引入了 Android 开发,当时还只支持单向绑定,而且需要作为第三方依赖引入,时隔一年,双向绑定这个特性也得到支持,同时纳入了 Android Gradle Plugin(1.5.0+) 中,只需要在 gradle 配置文件中添加三行代码,就能用上数据绑定。
android {
dataBinding {
enabled true
}
}
使用数据绑定的优点
- 能有效提高开发效率,减少大量需要手动编写的胶水代码(如
findViewById
,setOnClickListener
) - 高性能(绝大部分的工作在编译器完成,避免运行时使用反射)
- 使用灵活(可以使用表达式在布局内进行一定的逻辑运算)
- 具有 IDE 支持(语法高亮、自动补全,语法错误标记)
举个简单的例子
需求:界面上有两个控件,EditText 用于获取用户输入,TextView 用于把用户输入展示出来。
传统实现:用传统的方式来实现,需要定义一个布局,设置好这两个控件,然后再代码中引用这个布局,把这两个控件找出来,然后添加监听器到 EditText 上,在输入发生变化的时候,获取输入,然后更新到 TextView 上。
而是用数据绑定,代码如下:
class MainAct : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ContentMainBinding = ContentMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
使用了数据绑定,代码逻辑结构变得清晰,手动编写的胶水代码得到了简化(有数据绑定框架替我们生成),数据绑定框架帮我们做了控件的数据变化监听,并将数据同步更新到控件上。
注意 如果是使用 Kotlin,需要在 app 的 build.gradle 文件中添加如下文件:
apply plugin: 'kotlin-kapt'
...
dependencies {
...
kapt "com.android.databinding:compiler:3.1.2"
}
数据绑定的使用
布局文件的改造
使用数据绑定的布局文件以
标签作为根节点,表明这是个数据绑定的布局,修改后数据绑定框架会生成对应的 *Binding 类,如 content_main.xml
会生成 ContentMainBinding 类,即默认规则是:单词首字母大写,移除下划线,并在最后添加上 Binding。
数据的声明和辅助类的声明
在
标签内部添加 标签,即可声明数据。给
标签添加
class
属性可以改变生成的 *Binding 类的名字,如使用 将其改为
ContentMain
。
数据标签内部通过
标签声明变量,通过
标签导入辅助类,为了避免同名冲突,可以使用 alias
属性指定一个别名。
数据绑定的使用
变量声明之后,就可以在布局中使用了,使用的方式和使用 Java 类似,当表达式使用一个对象内的属性时,会分别尝试直接调用、getter、ObservableField.get()。
数据绑定内支持表达式,可以使用表达式来进行一些基本的逻辑运算。
常用的操作有:
- 数学计算福:
+、-、*、/、%
- 字符串拼接:
+
- 逻辑运算符:
&&、||
- 比较运算符:
==、>、<、>=、<=
- 函数调用
- 类型转换
- 数据存取
[]
,对容器类的操作支持使用这种方式来存取 - Null 合并运算符:
??
,合并运算符会在变量非空的时候使用左边的操作,反之使用右边的,
如data ?? data.defaultVal
事件绑定
严格意义上来说,事件绑定也属于数据绑定的一种。之前我们常在布局内进行的 android:onClick="onBtnClick"
就可以视作一种数据绑定。但通过使用数据绑定框架,允许做更多事情。
可以通过数据绑定,传入一个变量,调用该变量上的方法用于事件的处理,跟原有的方式对比,数据绑定允许我们将处理事件的逻辑和布局所关联的类解耦,可以方便的替换不同的处理逻辑。
也可以通过表达式,在布局内直接执行一些代码,不需要我们切换回 Java 代码中去实现,对于一些不需要外部处理,仅仅是布局内相关的逻辑来说,这种特性允许我们把 UI 相关的逻辑进行内聚。
数据绑定框架的另一个特性,在进行数据相关的操作前,会检查变量是否为空,倘若没有传入对应的变量,或者控件为空,在布局上进行的操作并不会执行,因此,加入上述例子中,我们没有传入对应的 presenter 对象,点击按钮并不会引发 Crash。
还有,由于编译期会进行检查,加入对应的数据类型上没有实现对应的方法,或方法签名不对(参数类型应为 View),那么编译的时候就会报错,代码的稳定性也因此得到了保障。
数据模型
虽然数据绑定支持的 POJO(Pure Old Java Object,普通 Java 类,值仅具有一部分 getter/setter 方法的类),但对 POJO 对象的数据更新并不会同步更新 UI。为了实现自动更新,可以选择:
- 继承自
BaseObservable
,给getter
加上@Bindable
注解,并在setter
中实现域的变动通知; - 如果数据类无法继承
BaseObservable
,变动通知可以用PropertyChangeRegistry
来实现; - 最后一种是使用
Observable 域
,对数据存取通过ObservableField
的get
、set
方法调用实现。ObservableField
是泛型类,对于基础类型,有对应的ObservableInt
、ObservableLong
、ObservableShort
等可供使用;另外对于容器,每次优惠更新其中一项,而不是整个更新,因此还有对应的ObservableArrayList
、ObservableArrayMap
可供使用。
从使用上来说,第三种方式更加直观和便捷,需要人工接入的地方更少,更不容易出错,推荐使用。
数据绑定的原理
数据绑定相关类的初始化
首先需要找一个切入点,最显而易见的切入点便是 ContentMainBinding.inflate
,这个类似数据绑定框架生成的,生成的文件位于 build/intermediates/classes/debug/
目录下。
public static ContentMainBinding inflate(@NonNull android.view.LayoutInflater inflater) {
return inflate(inflater, android.databinding.DataBindingUtil.getDefaultComponent());
}
方法的实现调用了另一个 inflate
方法,经过几次辗转,最终调用到了 ContentMainBinding.bind
方法。
public static ContentMainBinding bind(@NonNull android.view.View view,
@Nullable android.databinding.DataBindingComponent bindingComponent) {
if (!"layout/content_main_0".equals(view.getTag())) {
throw new RuntimeException("view tag isn't correct on view:" + view.getTag());
}
return new ContentMainBinding(bindingComponent, view);
}
这个方法首先检查这个 view 是否是数据绑定相关的布局,不是则会抛出异常,是的话则实例化 ContentMainBinding
。
ContentMainBinding
实例化代码如下:
public ContentTestBinding(@NonNull android.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
super(bindingComponent, root, 0);
final Object[] bindings = mapBindings(bindingComponent, root, 6, sIncludes, sViewsWithIds);
this.fullName = (android.widget.TextView) bindings[5];
this.mboundView0 = (android.widget.LinearLayout) bindings[0];
this.mboundView0.setTag(null);
this.mboundView1 = (android.widget.Button) bindings[1];
this.mboundView1.setTag(null);
this.mboundView2 = (android.widget.EditText) bindings[2];
this.mboundView2.setTag(null);
setRootTag(root);
// listeners
mCallback1 = new android.databinding.generated.callback.OnClickListener(this, 1);
invalidateAll();
}
构造函数内首先调用 mapBindings
把 root
中所有的 view 找出来,数字 3 指的是布局中总共有 8 个 view,然后还传入 sIncludes
和 sViewWithIds
,前者是布局中 include 进来的布局的索引,后者是布局中包含 id 的索引。
这两个参数是静态变量,初始化如下:
@Nullable
private static final android.databinding.ViewDataBinding.IncludedLayouts sIncludes;
@Nullable
private static final android.util.SparseIntArray sViewsWithIds;
static {
sIncludes = null;
sViewsWithIds = new android.util.SparseIntArray();
sViewsWithIds.put(R.id.fullName, 5);
}
由于 Demo 中的布局不包含 include,因此 sIncludes
被值为 null,而布局内有一个 id 为 R.id.fullName
的控件,因此他被加入到 sViewsWithIds
中,5 表示他在 bindings
中的索引。
再回到构造函数,mapBindings
查找到的 View 都防止在 bindings
这个数组中,并通过生成代码的方式,将它们一一取出来,转化为对应的数据类型,有设置 id 的控件,会以 id 作为变量名,没有设置 id 的控件,则以 mboundView + 数字
的方式依次赋值。然后将这个 Binding 和 肉体 关联起来(通过将 Binding 设为 rootView 的 tag 的方式)。
还实例化了一个 OnClickListener
,用于绑定事件响应。
mapBindings
的方法实现在 ViewDataBinding
这个类里面,主要是把 root 内所有的 view 给查找出来,并放置到 bindings
对应的索引内,这个索引如何确定呢?原来,数据绑定在处理布局的时候,生成了辅助信息在 view 的 tag 里,通过解析这个 tag,就能知道对应的索引了。所以,为了避免自己 inflate 布局之后,不小心操作了 view 的 tag 对解析产生干扰,尽量使用数据绑定来得到 inflate 之后 的 view。处理过的布局片段如下,生成位置为 app/build/intermediates/data-binding-layout-out/
目录。
mapBindings
方法比较长,里面针对不同情况进行了处理,虽然这个方法看似使用了递归,但实际上是通过这种方式实现对 root 下所有的控件的遍历,因此整个方法的时间复杂度是 O(n),通过一次遍历,找到所有空间,整体性能比使用 findViewById
还优秀。
实例化的 OnClickListener
接受两个参数,一个是 OnClickListener.listener
,ContentMainBinding
实现了这个接口,所以第一个参数传的值是 ContentMainBinding
,另一个是标识这个 listener 作用的控件的 sourceId
。这个 OnClickListener
干的事情很简单,就是把点击事件,附加上 sourceId
,回传给了 ContentMainBinding
的 _internalCallbackOnClick
处理,也就是最后所有跟布局相关的操作逻辑内聚到了 ContentMainBinding
这个类中。
// callback impls
public final void _internalCallbackOnClick(int sourceId, android.view.View callbackArg_0) {
// localize variables for thread safety
// firstName
java.lang.String firstName = mFirstName;
// (firstName) + ('·')
java.lang.String firstNameChar = null;
// lastName
java.lang.String lastName = mLastName;
// ((firstName) + ('·')) + (lastName)
java.lang.String firstNameCharLastName = null;
if ((fullName) != (null)) {
firstNameChar = (firstName) + ('·');
firstNameCharLastName = (firstNameChar) + (lastName);
fullName.setText(firstNameCharLastName);
}
}
从实现可以看到,这里仅仅实现了在布局中写下的内部处理逻辑 () -> fullName.setText(firstName + lastName)
,由于布局中这样的处理逻辑仅有一处,所以这里 sourceId 没有使用到。如果有多于 2 处的逻辑,这里会生成一个 switch
块,通过 sourceId 执行不同的指令。从实现还可以看到,框架生成的代码使用本地变量来持有成员变量,以保证对变量的访问是线程安全的。同样的,在对访问控件之前,会进行是否为空的检查,避免空指针错误。这也是使用数据绑定的带来的好处:通过框架自动生成的代码中的为空检查,避免手工编码容易导致的空指针错误。
但是构造函数这里仅仅是创建了监听器,并没有将它 set 到对应的控件中去。
数据绑定的 Rebind 机制
在构造函数的最后,调用了方法 invalidateAll
。
@Override
public void invalidateAll() {
synchronized(this) {
mDirtyFlags = 0x8L;
}
requestRebind();
}
invalidateAll
方法将脏标记位 mDirtyFlags
标记为 0x8L
,这个脏标记位是一个 long 的值,也就是最多有 64 个位可供使用。由于 mDirtyFlags
这个变量是成员变量,且多处会对其进行写操作,所以对他的写操作都是同步进行的。更新完这个值,紧接着就调用 requestRebind
方法,请求执行 rebind 操作。
这个方法的实现在 ContentMainBinding
的基类 ViewDataBinding
中。
protected void requestRebind() {
if (mContainingBinding != null) {
mContainingBinding.requestRebind();
} else {
synchronized (this) {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
}
if (mLifecycleOwner != null) {
Lifecycle.State state = mLifecycleOwner.getLifecycle().getCurrentState();
if (!state.isAtLeast(Lifecycle.State.STARTED)) {
return; // wait until lifecycle owner is started
}
}
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
}
}
如果此前没有请求执行 rebind 操作,那么会将 mPendingRebind
置为 true
,API 等级 16 及以上,会往 mChoreographer
发一个 mFrameCallback
,在系统刷新界面 (doFrame
) 的时候执行 rebind 操作,API 16 以下,则是往 UI 线程 post 一个 mRebindRunnable
任务。mFrameCallback
的内部实际上调用的是 mRebindRunnable
的 run
方法,因此这两个任务除了调用时机,干的事情其实没什么不同。
而如果此前请求过执行 rebind 操作,即已经 post 了一个任务到队列去,而且这个任务还未获得执行,此时 mPrendingRebind
的值为 true
,那么 requestRebind
将直接返回,避免重复、频繁执行 rebind 操作带来的性能损耗。
private final Runnable mRebindRunnable = new Runnable() {
@Override
public void run() {
synchronized (this) {
mPendingRebind = false;
}
processReferenceQueue();
if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
// Nested so that we don't get a lint warning in IntelliJ
if (!mRoot.isAttachedToWindow()) {
// Don't execute the pending bindings until the View
// is attached again.
mRoot.removeOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
return;
}
}
executePendingBindings();
}
};
当任务获得执行时,立即将 mPendingRebind
设为 false
,以便后续其他 requestRebind
能往主线程发起 rebind 的任务。在 API 19 及以上的版本,检查下 UI 控件是否附加到了窗口上,如果没有附到窗口上,则设置监听器,以便在 UI 附加到窗口上的时候立即执行 rebind 操作,然后返回。当符合执行条件(API 19 以下或 UI 控件已经附加到窗口上)的时候,则调用 executePendingBindings
执行 binding 逻辑。
public void executePendingBindings() {
if (mContainingBinding == null) {
executeBindingsInternal();
} else {
mContainingBinding.executePendingBindings();
}
}
private void executeBindingsInternal() {
if (mIsExecutingPendingBindings) {
requestRebind();
return;
}
if (!hasPendingBindings()) {
return;
}
mIsExecutingPendingBindings = true;
mRebindHalted = false;
if (mRebindCallbacks != null) {
mRebindCallbacks.notifyCallbacks(this, REBIND, null);
// The onRebindListeners will change mPendingHalted
if (mRebindHalted) {
mRebindCallbacks.notifyCallbacks(this, HALTED, null);
}
}
if (!mRebindHalted) {
executeBindings();
if (mRebindCallbacks != null) {
mRebindCallbacks.notifyCallbacks(this, REBOUND, null);
}
}
mIsExecutingPendingBindings = false;
}
这里实际上还没有执行具体的 binding 操作,这里在执行前进行一些判定:
- 如果已经开始执行绑定操作了,即这段代码正在执行,那么调用一次
requestRebind
,然后返回 - 如果当前没有需要进行刷新 UI 的需要,即脏标记为 0,那么直接返回。
- 接下来在执行具体的
executeBindings
操作前,调用下mRebindCallbacks.notifyCallbacks
,通知所有回调说即将开始 rebind 操作,回调可以在执行的过程中,将mRebindHalted
置为true
,阻止executeBindings
的运行,拦截成功同样通过回调进行通知。 - 如果没有被拦截,
executeBindings
方法便得以运行,运行结束后,同样通过回调进行通知。
executeBindings
是个抽象方法,具体的实现在子类中。即跟 content_main.xml
相关的逻辑依旧内聚到了 ContentMainBinding
中。
executeBindings
的实现也是数据绑定框架在编译器生成的,如下:
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
// ... 省略变量声明
if ((dirtyFlags & 0x9L) != 0) {
if (presenter != null) {
// read presenter::onClick
presenterOnClickAndroidViewViewOnClickListener = (((mPresenterOnClickAndroidViewViewOnClickListener == null)
? (mPresenterOnClickAndroidViewViewOnClickListener = new OnClickListenerImpl())
: mPresenterOnClickAndroidViewViewOnClickListener).setValue(presenter));
}
}
if ((dirtyFlags & 0xaL) != 0) {
}
if ((dirtyFlags & 0xcL) != 0) {
}
// batch finished
if ((dirtyFlags & 0x9L) != 0) {
// api target 1
this.mboundView1.setOnClickListener(presenterOnClickAndroidViewViewOnClickListener);
}
if ((dirtyFlags & 0xcL) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, firstName);
}
if ((dirtyFlags & 0x8L) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView2, (android.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (android.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (android.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView2androidTextAttrChanged);
android.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView3, (android.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (android.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (android.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView3androidTextAttrChanged);
this.mboundView4.setOnClickListener(mCallback1);
}
if ((dirtyFlags & 0xaL) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView3, lastName);
}
}
实现中,首先把脏标记位存到本地变量中,然后将脏标记位置为 0,开始批量处理之前的改动。根据脏标记位和相关的值进行位与运算来判断。
这里做了:
- 创建并设置回调,如
android:onClick="@{presenter::saveUserName}"
这种表达式,会在presenter
不为空的情况下,创建对应的回调,并设置到相应的控件上; - 将数据模型上的值更新到 UI 上,如将
firstName
设置到mboundView1
上,lastName
设置到mBoundView2
上。可以看到,每一个
标签声明的变量都有一个专属的标记位,当改变量的值被更新时,对应的脏标记位就会置为 1,executeBindings
的时候变回将这些变动更新到对应的控件。 - 在设置了双向绑定的控件上,为其添加对应的监听器,监听其变动,如:EditText 上设置了 TextWatcher。具体的设置逻辑放置到了
TextViewBindingAdapter.setTextWatcher
里。源码如下,也是创建一个新的TextWatcher
,将创建来的监听器包裹在其中。
方法数的问题
data binding 框架的 jar 包有两个,一个是 adapter,一个是 baseLibrary,前者方法数为 415,后者方法数为 502,整体增加的方法数不到一千个。生成的类方法数方面 demo 中大约是每个布局 20 个方法,具体跟布局内的变量数量(每个变量对应一个 get、set 方法)、双向绑定的数量(每个会多一个 InverseBindingListener
匿名类),会根据这几个因素有所浮动。
小结
将数据绑定在应用内的运行机制总结如下:
- 通过对 root view 进行一次遍历,将 view 中所有的控件查找出来并进行绑定,查找效率比使用
findViewById
更加高效 - 查找过程依赖于 view 的 tag 标记,尽量避免使用 tag 标记,以免跟干涉到框架的正常运行
- 对 UI 的操作都在主线程;对数据的操作可以在任意线程
- 对数据的操作并不会即时的反应到 UI 上,通过脏标记,往主线程发起 rebind 任务,在主线程下次回调的时候批量刷新,避免频繁操作 UI
- 使用数据绑定操作 UI 更加安全,操作集中在主线程,并在操作前进行空检查,避免空指针
- 绝大部分的逻辑在生成的
*Binding
类中,即数据绑定框架在编译期帮我们做了大量的工作,生成模板代码,实现绑定逻辑,是否为空检查,生成代理类,代码的可靠性也是由编译期的处理程序保证,有效的降低了人为出错的可能