http://www.jianshu.com/p/de4d50b88437
DataBinding技术能解决什么问题?
DataBinding技术的出现,肯定是为了解决我们在开发中的一些痛点问题。所以,了解DataBinding要解决的问题,能够使我们更深刻地理解DataBinding技术的设计实现。
从开发角度看,简言之,DataBinding主要解决了两个问题:
- 需要多次使用findViewById,损害了应用性能且令人厌烦
- 更新UI数据需切换至UI线程,将数据分解映射到各个view比较麻烦
应该说,针对上述问题,都有第三方解决方案。第一个问题可以使用Jake Wharton 的ButterKnife;对于第二个问题,谷歌提供了Loop-Handler方案,你还可以使用RxJava,EventBus等方案,但它们只是解决了线程切换的问题,却没有解决将数据分解映射到各个view的问题,这正是DataBinding的魅力所在!同时,DataBinding的线程切换也是透明的,这是指,当你的Activity需要展示新的数据时,你可以在后台线程中获取数据,然后直接交给DataBinding就可以了,完全不需要关心线程切换的问题。
DataBinding如何解决这些问题?
总体思路
DataBinding解决这些问题的思路非常简单。就是针对每个Activity的布局,在编译阶段,生成一个ViewDataBinding类的对象,该对象持有Activity要展示的数据和布局中的各个view的引用(这里已经解决了令人厌烦的findViewById问题)。同时该对象还有如下可喜的功能:
- 将数据分解到各个view
-
在UI线程上更新数据
-
监控数据的变化,实时更新
有了这些功能,你会感觉到,你要展示的数据已经和展示它的布局紧紧绑定在了一起,这就是该技术叫做DataBinding的原因。
实现细节
下面,我们深入DataBinding的内部,看看它是如何实现以上所说的功能的。
如何设置使用DataBinding在此就不赘述了,网上大把大把的资料。
示范项目基本情况:
- 项目名称为 DataBindingTest
- 项目包名 com.like4hub.www.databindingtest
- 项目只有一个主Activity,名称为MainActivity,其布局文件为activity_main.xml
- 项目用到的图片资源有两个,如下:
-
项目中要展示的数据是User, 其代码如下:
package com.like4hub.www.databindingtest; public class User { private String firstName ; private String lastName ; private String avatar ; public User(String avatar, String firstName, String lastName) { this.avatar = avatar; this.firstName = firstName; this.lastName = lastName; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName =firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
有了以上的准备工作,我们可以开始了。
首先创建如下一个布局:
<layout xmlns:android="http://schemas.android.com/apk/res/android" >
<data>
<variable name="user"
type="com.like4hub.www.databindingtest.User"/>
</data>
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp" >
<TextView android:id="@+id/firstname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
<TextView android:id="@+id/lastname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@{user.lastName}" />
<ImageView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:src="@{@drawable/avatar_pure}" />
<Button android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/orange"
android:text="Test" />
</LinearLayout>
</layout>
我们看到,使用DataBinding需要遵照一定的模板去写布局文件,这个模板如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android" >
<data>
<!--此处定义该布局要用到的数据的名称及类型-->
</data>
<!--此处按照常规方式定义要使用的布局,其中可以使用binding表达式代表属性值,所谓binding表达式,指形如"@{user.firstName}"的表达式-->
</layout>
我们的Activity onCreate()方法是这样的:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_main);
User user = new User(null, "万","人迷" );
binding.setUser(user);
}
然后运行我们的程序,结果如下:
那么问题来了,DataBinding究竟在背后做了什么?下面,我们就分步骤进行讲解。
-
一、对布局文件进行预处理
首先,DataBinding会对根元素为<layout>的布局文件进行预处理(本例中即activity_main.xml),处理后,原布局文件会变成这个样子:
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:tag="layout/activity_main_0"
xmlns:android="http://schemas.android.com/apk/res/android" >
<TextView android:id="@+id/firstname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="binding_1" />
<TextView android:id="@+id/lastname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:tag="binding_2" />
<ImageView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:tag="binding_3" />
<Button android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/orange"
android:text="Test" />
</LinearLayout>
我们看到,根元素LinearLayout和那些在属性中使用了binding表达式的view都被设置了Tag,而原有的<layout>标签、data标签以及里面的variable标签,还有各个view中的binding表达式都不见了!!
DataBinding将它们藏在哪儿了呢?答案是:DataBinding把最初布局文件中的<data>以及各个view中的binding表达式内容抽取出来,生成了一个名为activtiy_main-layout.xml文件,该文件主要内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Layout layout="activity_main"
modulePackage="com.like4hub.www.databindingtest"
>
<Variables declared="true" type="com.like4hub.www.databindingtest.User" name="user">
</Variables>
<Targets>
<Target tag="layout/activity_main_0" view="LinearLayout">
</Target>
<Target id="@+id/firstname" tag="binding_1" view="TextView">
<Expression text="user.firstName" attribute="android:text"/>
</Target>
<Target id="@+id/lastname" tag="binding_2" view="TextView">
<Expression text="user.lastName" attribute="android:text"/>
</Target>
<Target tag="binding_3" view="ImageView">
<Expression text="@drawable/avatar_pure" attribute="android:src"/>
</Target>
</Targets>
</Layout>
通过给原有布局文件中的view设置Tag和在生成的文件中(本例中即activtiy_main-layout.xml)使用Tag,使得抽取出来的内容能够与其原先所在的位置对应起来。如下图所示:
这里有几点需要注意:
1、LinearLayout设置的Tag是以layout开头的,表示它是根布局。
2、最初布局文件<data>标签中的内容几乎原封不动的挪到了新生成的文件中。
- 二、生成ActivityMainBinding与BR类
现在,DataBinding将会依据上面两个xml文件(即activtiy_main.xml和activtiy_main-layout.xml)生成两个类,一个类是ActivityMainBinding,它继承自ViewDataBinding,里面包含如下fields:
// views
public final android.widget.Button button;
public final android.widget.TextView firstname;
public final android.widget.TextView lastname;
private final android.widget.LinearLayout mboundView0;
private final android.widget.ImageView mboundView3;
// variables
private com.like4hub.www.databindingtest.User mUser;
观察这些fields,我们可以发现:
对应每个variable标签,ActivityMainBinding都有一个相应的变量,在本例中就是上面的mUser变量。
对应每一个有id的View,都会有一个以其id为名的public final变量,其类型正是该View的类型(如button,firstname)。
对应每一个没有id但是处理中添加了Tag 的View,都会有一个private final的变量与其对应,名字没有什么特殊的含义(如mboundView0,mboundView3)。
生成的BR类的内容非常简单,如下:
package com.like4hub.www.databindingtest;
public class BR {
public static final int _all = 0;
public static final int user = 1;
}
其中的_all变量是默认生成的,user变量是对应ActivityMainBinding类中的mUser变量的。举例来讲,假如我们有一个ActivityMainBinding类的实例对象amb,我们可以调用amb.setVariable(BR.user, userInstance)
,该调用将会把userInstance赋值给amb的mUser变量。下面是setVariable方法的代码:
public boolean setVariable(int variableId, Object variable) {
switch(variableId) {
case BR.user : setUser((com.like4hub.www.databindingtest.User)
variable);
return true;
}
return false;
}
那么,DataBinding是否仅仅只给<data>标签中的每一个variable生成对应的BR常量,答案是:NO。
如果你在User类中的getAvatar方法上添加@Bindable注解,并且让User类继承BaserObservable那么,DataBinding生成的BR类中将会是这样:
public class BR {
public static final int _all = 0;
public static final int avatar = 1;
public static final int user = 2;
}
实际上,BR中的常量是一种标识符,它对应一个会发生变化的数据,当数据改变后,你可以用该标识符通知DataBinding,很快,DataBinding就会用新的数据去更新UI。
那么,DataBinding如何知道哪些数据会变化呢?目前,我们可以确定,<data>中的每一个variable是会变化的,所以DataBinding会为它们生成BR标识符。用@Bindable 注解的类中的getXXX方法(该类父类为BaseObservable或者实现Observable接口)对应一个会变化的数据,DataBinding也会为它们生成BR标识符。实际上,还有第三种,暂且按下不表。
- 三、生成ActivityMainBinding实例并绑定
在这一步中,主要有三个过程:
第一步就是Inflate 处理后的布局文件,由于现在activity_main.xml文件与普通的layout文件一样。现在DataBindingUtil将会Inflate activity_main.xml文件,得到一个ViewGroup变量root。
第二步就是生成ActivityMainBinding实例对象,DataBindingUtil会将这个变量root传递给ActivityMainBinding的构造方法,生成一个ActivityMainBinding的实例,就是我们在onCreate方法中获取的binding对象。下面看看ActivityMainBinding的构造过程,它的构造方法签名如下:public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root)
其中第二个参数就是刚刚生成的ViewGroup root。你可能想知道第一个参数bindingComponent哪来的,简单一句话,是从DataBindingUtil的getDefaultComponent调用中得来的。如果你之前学习过DataBinding,并且使用过BindingAdapter的话,你应该会比较熟悉它,这里不展开讲。
好,让我们继续构造我们的ActivityMainBinding对象。
在构造方法中,ActivityMainBinding会首先遍历root,根据各个View的Tag或者id,初始化自己的fields,就是下面这些:
public final android.widget.Button button;
public final android.widget.TextView firstname;
public final android.widget.TextView lastname;
private final android.widget.LinearLayout mboundView0;
private final android.widget.ImageView mboundView3;
至此,Tag们的历史使命完成了,ActivityMainBinding将会把之前加到各个View上的Tags清空。
最后,构造方法调用invalidateAll引发数据绑定。
第三步就是进行数据绑定
在这一步中,ActivityMainBinding将会计算各个view上的binding表达式,然后赋值给view相应的属性。绑定的主要代码如下(省略部分细节):
@Override
protected void executeBindings() {
java.lang.String firstNameUser = null;
java.lang.String lastNameUser = null;
com.like4hub.www.databindingtest.User user = mUser;
if (user != null) {
// read user.firstName
firstNameUser = user.getFirstName();
// read user.lastName
lastNameUser = user.getLastName();
}
TextViewBindingAdapter.setText(this.firstname, firstNameUser);
TextViewBindingAdapter.setText(this.lastname, lastNameUser);
ImageViewBindingAdapter.setImageDrawable(this.mboundView3,
getDrawableFromResource(R.drawable.avatar_pure));
}
下面我们来分析上面的数据绑定过程。首先,针对两个binding表达式
user.firstname 和 user.lastname ,ActivityMainBinding生成了两个临时变量,即:
java.lang.String firstNameUser = null;
java.lang.String lastNameUser = null;
从中我们可以看出这两个变量的命名的规律。这两个变量就代表了两个binding表达式的值,为它们赋值的过程实际上就是binding表达式求值的过程。
ActivityMainBinding通过调用mUser的getFirstName和getLastName方法为上面两个变量赋值。
请思考,ActivityMainBinding是怎么知道调用mUser的getXXX方法为binding表达式求值的?
这个问题可以分成两步:
首先,在构建ActivityMainBinding类时,会对activtiy_main-layout.xml中的数据进行分析,我们再次贴出该文件的内容,以便继续:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Layout layout="activity_main"
modulePackage="com.like4hub.www.databindingtest"
>
<Variables declared="true" type="com.like4hub.www.databindingtest.User" name="user">
</Variables>
<Targets>
<Target tag="layout/activity_main_0" view="LinearLayout">
</Target>
<Target id="@+id/firstname" tag="binding_1" view="TextView">
<Expression text="user.firstName" attribute="android:text"/>
</Target>
<Target id="@+id/lastname" tag="binding_2" view="TextView">
<Expression text="user.lastName" attribute="android:text"/>
</Target>
<Target tag="binding_3" view="ImageView">
<Expression text="@drawable/avatar_pure" attribute="android:src"/>
</Target>
</Targets>
</Layout>
DataBinding发现,有一个variable名为user,所以它为ActivityMainBinding生成了一个mUser变量,DataBinding进一步检查该文件发现,两个binding表达式user.firstName和user.lastName圆点前面的字符串也是user,由此知道,这两个表达式的值来自mUser。
接着,DataBinding再次进行分析,两个binding表达式圆点后的字符串分别是firstName和lastName,所以DataBinding决定调用mUser的getFirstName和getLastName方法。
请注意,让User类中包含这两个方法是我们开发者的责任。
求出值之后就是设置了,比较简单。
在这里我们可以清楚地看到,binding表达式user.firstName和user.lastName并不是对应着User类中的两个fields,它们实际对应的是User类的两个get方法。
至此,你可以大胆猜测一下,如果我们给User类添加一个如下方法:
public String getAlias(){
return "Alias";
}
但是我们并不给它添加一个String 类型的alias field,我们是否可以在binding表达式中这样写:@{user.alias}。
答案是:YES YOU CAN!
进一步你可以理解,上文中,我们为什么要将@Bindable注解加到一个get方法上面而不是一个field上面了。
最后,由于ImagView中的binding表达式本身就是一个值,我们不需要再求值了,直接赋值就是。本文这样做,仅仅是为了说明,DataBinding为View添加tag的规则是该View的属性中有没有使用binding表达式。
好了,至此,我们分析DataBinding工作的核心原理,还有三个内容没有涉及,一个是数据更新(仅略提了一下),另一个是BindingAdapter(其实在executeBindings方法中已经看到它们的身影了),最后一个是事件监听绑定(这个很简单)。其实,掌握了这些核心原理,剩下的内容你可以很轻松地掌握。