A man can be destroyed but not defeated. —— Daily English
DataBinding是一种工具,能在编译时绑定布局和对象。通过这篇文章,一是要掌握DataBinding的使用,二是我们要弄懂,View层是怎么改变Model的,而Model层又是如何改变View的。
介绍
APT预编译方式
我们已经知道,DataBinding里面的功能类是通过APT的工具来产生的。
APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。简单来说就是在编译期,通过注解生成.java文件。
当你的xml用DataBinding规定的格式去书写的时候,DataBinding就能够通过APT的技术,帮你生成对应的类文件。
Rebuild完成后,会在build
目录下生成以[布局名]Binding.java
文件,这里我们的布局叫activity_main
,所以生成了一个名为ActivityMainBinding.Java
类文件,以及继承自它的ActivityMainBindingImpl.java
类文件。
生成文件的具体位置参考下图
布局的格式和处理
我们需要规定的格式来定义我们的xml布局文件,参考布局activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- DataBinding编码规范 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 定义该View(布局)需要绑定的数据来源 -->
<data>
<variable
name="user"
type="pers.owen.databinding.model.UserInfo" />
</data>
<!-- 布局常规编码 -->
<LinearLayout...>
<EditText ... />
<EditText ... />
</LinearLayout>
</layout>
<data>
标签中绑定的可以是一个实体类,也可以是一个ViewModel,只要按照规定的格式去写,就可以。
在编译时,DataBinding会内部处理布局文件控件,重新生成两个全新的布局文件。一个与原文件同名,叫activity_main.xml
,另一个是名为activity_main-layout.xml
。

生成文件的具体位置参考下图
生成后的activity_main.xml
就跟我们普通的xml文件一样,用于Android os的渲染。但是DataBinding会在每个控件上都加上Tag,这个Tag就是用来快速查找和定位具体控件的。
生成后的activity_main.xml
代码如下
<?xml version="1.0" encoding="utf-8"?>
<!-- DataBinding编码规范 -->
<!-- 定义该View(布局)需要绑定的数据来源 -->
<!-- 布局常规编码 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical" android:tag="layout/activity_main_0" xmlns:android="http://schemas.android.com/apk/res/android">
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="binding_1" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:tag="binding_2" />
</LinearLayout>
生成的activity_main-layout.xml
文件,是DataBinding生成的配置文件,它能够通过文件中的配置信息,快速定位哪一个控件,该显示什么信息。
activity_main-layout.xml
代码如下
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="C:\workspace\adrproject\MyDataBinding\app\src\main\res\layout\activity_main.xml"
isBindingData="true"
isMerge="false" layout="activity_main" modulePackage="pers.owen.databinding"
rootNodeType="android.widget.LinearLayout">
<Variables name="user" declared="true" type="pers.owen.databinding.model.UserInfo">
<location endLine="7" endOffset="57" startLine="5" startOffset="8" />
</Variables>
<Targets>
<Target tag="layout/activity_main_0" view="LinearLayout">
<Expressions />
<location endLine="27" endOffset="18" startLine="11" startOffset="4" />
</Target>
<Target tag="binding_1" view="EditText">
<Expressions>
<Expression attribute="android:text" text="user.name">
<Location endLine="20" endOffset="39" startLine="20" startOffset="12" />
<TwoWay>true</TwoWay>
<ValueLocation endLine="20" endOffset="37" startLine="20" startOffset="29" />
</Expression>
</Expressions>
<location endLine="20" endOffset="42" startLine="17" startOffset="8" />
</Target>
<Target tag="binding_2" view="EditText">
<Expressions>
<Expression attribute="android:text" text="user.pwd">
<Location endLine="26" endOffset="38" startLine="26" startOffset="12" />
<TwoWay>true</TwoWay>
<ValueLocation endLine="26" endOffset="36" startLine="26" startOffset="29" />
</Expression>
</Expressions>
<location endLine="26" endOffset="41" startLine="22" startOffset="8" />
</Target>
</Targets>
</Layout>
布局这块,我们需要做的就是按照DataBinding要求的格式,定义好布局即可,其他都会在Rebuild的时候,自动生成。
关联Activity组件与布局
我们通过代码DataBindingUtil.setContentView(Activity,Layout);
来关联Activity组件与布局。有人可能会疑问,为什么不直接在Activity中setCentView。那是因为DataBinding需要Activity来获取根布局,到时候View层刷新后会通过根布局来查找并更新相应的控件。
何时生成设置Model帮助类?
在Rebuild的时候DataBinding会扫描所有Module中的xml文件。只有当你的xml布局中有符合DataBinding定义规范的<data>
标签,才会去生成对应的帮助类。
DataBinding TestActivity.java
<–> activity_ databinding_test.xml
--> ActivityDatabindingTestBinding
--> ViewDataBinding
实战
我们这边写一个小Demo,实现DataBinding的双向数据绑定即可。
引入
在module的build.gradle文件中引入DataBinding
android {
...
// 添加DataBinding依赖
dataBinding{
enabled = true
}
)
定义实体类
定义实体类,可以定义成原始的属性格式,也可以定义成被观察者属性格式。
我们先试一下定义成原始属性格式的实体类,也就是我们之前定义实体类的格式。
public class UserInfo extends BaseObservable {
private String name;
private String pwd;
@Bindable
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
@Bindable
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
notifyPropertyChanged(BR.pwd);
}
}
注意三点,1、继承BaseObservable
类,2、get()
方法上加上@Bindable
注解,3、set()
方法中加入notifyPropertyChanged
。
定义Layout布局
页面很简单就不截图了,两个EditText,分别对应UserInfo中的name和pwd。
<?xml version="1.0" encoding="utf-8"?>
<!-- DataBinding编码规范 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 定义该View(布局)需要绑定的数据来源 -->
<data>
<variable
name="user"
type="pers.owen.databinding.model.UserInfo" />
</data>
<!-- 布局常规编码 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={user.name}" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@={user.pwd}" />
</LinearLayout>
</layout>
Rebuild Project
Rebuild完成后,就会ActivityMainBinding.Java
文件和ActivityMainBindingImpl.java
文件,生成目录可参看本文开头部分。
书写代码绑定
public class MainActivity extends AppCompatActivity {
private UserInfo userInfo = new UserInfo();
private final static String TAG = "TAG >>>";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// 尝试1:单向绑定第一种方式:<Model -- View>
userInfo.setName("Owen");
userInfo.setPwd("123");
binding.setUser(userInfo);
Log.e(TAG, userInfo.getName() + " / " + userInfo.getPwd());
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
userInfo.setName("Sara");
userInfo.setPwd("222");
Log.e(TAG, userInfo.getName() + " / " + userInfo.getPwd());
}
}, 2000);
}
}
通过DataBindingUtil.setContentView(this, R.layout.activity_main)
绑定布局,获取到ActivityMainBinding
对象binding
,再通过binding.setUser(userInfo)
绑定实体类。
我们做了尝试1,给UserInfo的Name和Pwd设值,用Handler延时2s后重新改变model的属性值,来查看与之绑定的View是不是会发生变化。效果图如下:
我们看到结果是符合我们预期的,验证了Model层JavaBean中的属性值改变,会更新到View层的控件上。
定义被观察属性的实体类
我们前面说过了,实体类的定义还有第二种方式,格式如下:
public class UserInfo {
public ObservableField<String> name = new ObservableField<>();
public ObservableField<String> pwd = new ObservableField<>();
}
显然,这种方式直接把属性定义成ObservableField<T>
,这是DataBinding为我们提供的,大大减少了实体类定义的复杂度。
定义实体类的时候,我们需要记住两点规则:一个是有被观察者属性,第二个就是有刷新属性的方法。
在原生的实体类中,我们需要自己去继承,去加注解,调用notifyPropertyChanged
去实现。而DataBinding为我们提供的ObservableField<T>
本身就是一个观察者属性。
这种定义方式对实体类属性有特定的取值和赋值方式,我们来看第二个尝试
public class MainActivity extends AppCompatActivity {
private UserInfo userInfo = new UserInfo();
private final static String TAG = "TAG >>>";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// 尝试1:单向绑定第一种方式:<Model -- View>
...
// 尝试2:单向绑定第二种方式:<Model -- View>
userInfo.name.set("Owen");
userInfo.pwd.set("123");
binding.setUser(userInfo);
Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
userInfo.name.set("Sara");
userInfo.pwd.set("222");
Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
}
}, 2000);
}
}
运行效果和尝试1完全一致。
双向的数据绑定
以上都是Model层的数据改变,更新View层的显示,也就是所谓的单向绑定。我们来做第三个尝试,测试数据的双向绑定,不仅看Model–>View 的更新,也来看一下View层数据的改变,是不是也能更新其绑定Model中属性的值。
public class MainActivity extends AppCompatActivity {
private UserInfo userInfo = new UserInfo();
private final static String TAG = "TAG >>>";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// 尝试1:单向绑定第一种方式:<Model -- View>
...
// 尝试2:单向绑定第二种方式:<Model -- View>
...
// 尝试3:双向绑定(Model --- View View --- Model)
userInfo.name.set("Owen");
userInfo.pwd.set("123");
binding.setUser(userInfo);
Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
}
}, 6000);
}
}
我们先给UserInfo,设置Name和Pwd,检测是否会更新到View上。然后利用Handler延迟6s后重新读取UserInfo的属性。在这6s内,我们需要在界面上修改Name和Pwd对应的EditText值,来进行验证。
效果如下
logcat控制台输出
这个结果是符合预期的,借助DataBinding成功实现了双向的数据绑定。
如果View到Model更新失败的同学,可以参考排查xml布局文件中,android:text="@={user.name}"
处@
后面的=
是否有加上。
文中Demo会在文末给出。
源码分析
绑定过程
我们从DataBindingUtil.setContentView(this, R.layout.activity_main)
方法开始,跟踪这个绑定的过程。
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
↓
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId) {
return setContentView(activity, layoutId, sDefaultComponent);
}
↓
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
...
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
↓
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
...
return bind(component, childView, layoutId);
}
↓
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots,
int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId);
}
绑定的过程最后跟踪到了sMapper.getDataBinder(bindingComponent, roots, layoutId)
方法,我们来看这个方法是如何实现的。查找该方法的实现。
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
...
if ("layout/activity_main_0".equals(tag)) {
return new ActivityMainBindingImpl(component, view);
}
...
}
activity_main_0
是不是很熟悉。我们已经知道,在Rebuild项目的时候,我们xml文件会被重新生成两个xml文件。生成后的activity_main.xml
中所有的控件,都被添加了tag属性,而最外层layout的tag就是用[布局名]_0
组成的。可参考本文章前段部分关于生成新xml的代码。
所以当找到我们常规的布局文件后,就开始new ActivityMainBindingImpl(component, view)
创建DataBinding的功能类,开始搞事情。
我们来看ActivityMainBindingImpl
类构造方法的实现。
private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 2
);
this.mboundView0 = (android.widget.LinearLayout) bindings[0];
this.mboundView0.setTag(null);
this.mboundView1 = (android.widget.EditText) bindings[1];
this.mboundView1.setTag(null);
this.mboundView2 = (android.widget.EditText) bindings[2];
this.mboundView2.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
标记1。我们可以看到这里是有一个Object[] bindings
数组来记录我们布局文件中的三个控件。分别设置给了类中的三个成员变量mboundView0
,mboundView1
和mboundView2
。
然后执行invalidateAll()
,我们对该方法做一个跟踪。
@Override
public void invalidateAll() {
....
requestRebind();
}
↓
protected void requestRebind() {
....
mUIThreadHandler.post(mRebindRunnable);
}
标记2。我们看到,DataBinding会定义一个Handler去执行mRebindRunnable
,这个mRebindRunnable
里面也就是我们V和M双向绑定的核心核能。我们来看这个mRebindRunnable
是如何实现的。
private final Runnable mRebindRunnable = new Runnable() {
...
executePendingBindings();
};
↓
public void executePendingBindings() {
...
executeBindingsInternal();
}
↓
private void executeBindingsInternal() {
...
executeBindings();
...
}
mRebindRunnable
最终会执行到executeBindings()
方法。executeBindings()
方法是在ActivityMainBindingImpl
中实现的,代码如下。
@Override
protected void executeBindings() {
...
androidx.databinding.ObservableField<java.lang.String> userName = null;
java.lang.String userPwdGet = null;
pers.owen.databinding.model.UserInfo user = mUser;
java.lang.String userNameGet = null;
androidx.databinding.ObservableField<java.lang.String> userPwd = null;
...
userNameGet = userName.get();
...
userPwdGet = userPwd.get();
...
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, userNameGet);
...
androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView1, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView1androidTextAttrChanged);
androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView2, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView2androidTextAttrChanged);
...
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, userPwdGet);
...
}
最后这四行代码,短的这两行是setText()
,作用是把从JavaBean属性中读出来的值,设置到View上。也就是实现了Model到View更新。长的这两行是setTextWatcher()
,作用可想而知,就是为View控件设置监听,以改变对应JavaBean的属性的值。这样也就实现了View到Model的更新。
我们来看这两个监听中其中一个mboundView1androidTextAttrChanged
是如何定义的。
private androidx.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new androidx.databinding.InverseBindingListener() {
@Override
public void onChange() {
...
java.lang.String callbackArg_0 = androidx.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
...
userName.set(((java.lang.String) (callbackArg_0)));
...
};
把多余的代码去掉,逻辑不要太清晰。
至此,我们就完全弄懂了DataBinding的绑定过程。知道了,View层是怎么改变Model的,而Model层又是如何改变View的,这个数据双向绑定过程。
整个过程中,没有反射!
那这个mRebindRunnable
又是何时被执行的呢,换句话说,我们这个双向数据的更新动作是什么时候执行的呢?
我们找到mRebindRunnable
调用的位置,在ViewDataBinding
的静态代码块,会有一个全局的监听,这个监听也就是用来通知数据更新的。
static {
if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
ROOT_REATTACHED_LISTENER = null;
} else {
ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
@TargetApi(VERSION_CODES.KITKAT)
@Override
public void onViewAttachedToWindow(View v) {
// execute the pending bindings.
final ViewDataBinding binding = getBinding(v);
binding.mRebindRunnable.run();
v.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewDetachedFromWindow(View v) {
}
};
}
}
标记3。每当View改变的时候,这个监听中的回调方法就会被执行,通过一个Handler去post我们的mRebindRunnable
,从而更新Model。反之亦然。
至此,绑定过程分析完毕。
内存消耗简析
我在绑定过程中标记了三个标记,想必大家都注意到了。这三个标记就是需要额外消耗内存的地方。
标记1:额外数组
定义了一个额外的数组来记录这些控件。数组造成的额外内存开销。
标记2:Runable
只要当控件发生任何的数据改变,它都有一个监听。在这个监听中,Binder会new一个Runable。
每个Activity都会有一个Runable,10个Activity就会创建10个Runable。
标记3:handler的loop一直在等待状态。
一旦Model的数据刷新,最后都是通过Handler去刷新UI的。我们知道Handler中有一个管道,会一直等待消息进来,然后通过Lopper.loop()方法,去取消息处理。当有消息了,就通过这个Tag找到控件,为控件赋值。Handler的等待造成的额外内存开销。
本文完!相信坚持看完本文的同学一定会有收获。
文中Demo下载地址。
Android 架构设计模式系列文章索引
本系列文章引导页点击这里