声明:本篇文章已授权微信公众号 guolin_blog(郭霖)独家发布!
关于DataBinding技术,网上教程可谓多矣,但大都没能摆脱简单拷贝、翻译修改官方文档的嫌疑,个别的翻译的还不准确,初学者很容易被误导。鉴于此,本文结合本人项目实践中的经验与思考,为广大Android 开发者提供一篇有观点、有思考的DataBinding讲解文章。
声明:本文为作者原创,如需转载请自取,仅需在文章开头注明本人账号milter、原文链接并确保文章内容不被修改即可
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; }
}
有了以上的准备工作,我们可以开始了。
首先创建如下一个布局:
我们看到,使用DataBinding需要遵照一定的模板去写布局文件,这个模板如下:
我们的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会对根元素为
我们看到,根元素LinearLayout和那些在属性中使用了binding表达式的view都被设置了Tag,而原有的
DataBinding将它们藏在哪儿了呢?答案是:DataBinding把最初布局文件中的以及各个view中的binding表达式内容抽取出来,生成了一个名为activtiy_main-layout.xml文件,该文件主要内容如下:
通过给原有布局文件中的view设置Tag和在生成的文件中(本例中即activtiy_main-layout.xml)使用Tag,使得抽取出来的内容能够与其原先所在的位置对应起来。如下图所示:
这里有几点需要注意:
1、LinearLayout设置的Tag是以layout开头的,表示它是根布局。
2、最初布局文件标签中的内容几乎原封不动的挪到了新生成的文件中。
- ** 二、生成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是否仅仅只给标签中的每一个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如何知道哪些数据会变化呢?目前,我们可以确定,中的每一个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中的数据进行分析,我们再次贴出该文件的内容,以便继续:
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方法中已经看到它们的身影了),最后一个是事件监听绑定(这个很简单)。其实,掌握了这些核心原理,剩下的内容你可以很轻松地掌握。
广告时间:
最近开通了视频号[ 工程师milter ],内容以互联网面试招聘,职场发展,八卦趣闻等为主。欢迎添加milter微信:18518722171