一、前言
DataBinding库其实出来挺久了,最近工作涉及到跨平台开发,接触到前端的响应式编程,比如微信小程序开发,Vue框架,使用起来还是比较方便的,开发效率也比较高,因此深入分析了Android中的DataBinding库的使用和原理。
二、使用
1. 简单入门
先从最简单的入手,为了让项目支持使用DataBinding,需要在build.gradle中打开开关:
android {
......
dataBinding {
enabled true
}
}
然后写个简单的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="text"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="@{text}"/>
</LinearLayout>
</layout>
最后在Activity的onCreate中设置text的值:
public class BasicActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityBasicBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_basic);
binding.setText("hello world");
}
}
这样最简单的数据绑定demo就写好了,代码通过setText设置text的值,UI就会自动进行刷新。
2. 表达式
可以看到,在布局文件中,使用数据绑定用的是@{}符号,而中间就是具体的变量,其实这里也可以支持各种表达式的,比如+-/*%,&& || 数组访问等等,官方给出支持表达式如下:
算术运算符 + - / * %
字符串连接运算符 +
逻辑运算符 && ||
二元运算符 & | ^
一元运算符 + - ! ~
移位运算符 >> >>> <<
比较运算符 == > < >= <=(请注意,< 需要转义为 <)
instanceof
分组运算符 ()
字面量运算符 - 字符、字符串、数字、null
类型转换
方法调用
字段访问
数组访问 []
三元运算符 ?:
注意表达式如果涉及到字符串的,外层需要使用’’,而内层的字符串使用””,另外所有的<都必须写成<。
3. Observable
在上面的代码中,绑定的变量是一个String对象text,比较简单,但其实还可以绑定到更复杂的实体类中,比如user.name,如果User只是一个普通的实体类,我们修改里面的name,实际上并不会自动更新UI,而需要调用setUser才能更新,下面以RGB实体类为例进行说明:
Random random = new Random();
data.setR(random.nextInt(256));
data.setG(random.nextInt(256));
data.setB(random.nextInt(256));
// 这里必须重新设置才能触发更新
mBinding.setData(data);
这时我们可以改造RGB类,让它继承自BaseObservable,然后在get方法上加注解@Bindable,在set方法中调用notifyPropertyChanged通知字段发生改变,如下:
public class ObservableRGB extends BaseObservable {
private int r;
private int g;
private int b;
@Bindable
public int getR() {
return r;
}
public void setR(int r) {
this.r = r;
notifyPropertyChanged(BR.r);
}
@Bindable
public int getG() {
return g;
}
public void setG(int g) {
this.g = g;
notifyPropertyChanged(BR.g);
}
@Bindable
public int getB() {
return b;
}
public void setB(int b) {
this.b = b;
notifyPropertyChanged(BR.b);
}
}
这时就不再需要调用setData重新设置整个实体类对象了,只要r或者g或者b的值改变了,都会字段更新到UI上。
这种做法还是比较麻烦的,因为rgb都是int值,其实有更简单的方法,也就是用ObservableInt,直接把rgb都定义为ObservableInt引用就可以实现了,如下:
public class IntRGB {
private ObservableInt r = new ObservableInt();
private ObservableInt g = new ObservableInt();
private ObservableInt b = new ObservableInt();
private ObservableField<String> s = new ObservableField<>();
/**
* 注意这里必须返回ObservableInt对象,而不是int值
* @return
*/
public ObservableInt getR() {
return r;
}
public void setR(int r) {
this.r.set(r);
}
public ObservableInt getG() {
return g;
}
public void setG(int g) {
this.g.set(g);
}
public ObservableInt getB() {
return b;
}
public void setB(int b) {
this.b.set(b);
}
public ObservableField<String> getS() {
return s;
}
public void setS(String s) {
this.s.set(s);
}
}
注意这里有个坑,getXXX必须返回ObservableInt对象,而不能返回具体的值,不然会出现UI不刷新的问题。
4. 绑定事件
DataBinding也支持绑定事件,比如绑定onClick的回调方法,onWindowFocusChanged的回调方法等等,而且绑定时支持lambda表达式,回调方法支持自定义参数:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="方法引用方式"
android:onClick="@{eventHandler::onClick}"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="lambda表达式方式"
android:onClick="@{() -> eventHandler.onLambdaClick(context)}"
android:layout_marginTop="10dp"/>
这里的context是DataBinding内部实现好的,可以直接用,实际上就是对应的Activity对象。
5. BindingAdapter
在最开始的例子中,我们可能会想,DataBinding是如何知道android:text的值,要用setText方法去动态设置,这里其实就涉及到绑定查找顺序,DataBinding会先查找对应的set方法,比如text,就找setText,color就找setColor,如果找到就可以直接用了,如果找不到,就遍历BindingMethod注解,这个注解会声明xml属性对应哪个方法,如果还是没有,就会找被BindingAdapter注解的方法,该注解会指定该方法对应的属性,如果还是没有,就编译失败报错。
BindingMethod用法:
@BindingMethods({
@BindingMethod(type = TextView.class, attribute = "app:bindText", method = "setText")
})
public class BindingAdapterHelper {
......
}
这里bindText是自定义属性,通过BindingMethod注解,把它映射到setText方法上,最终会通过setText方法设置该值,注意BindingMethods和BindingMethod是一起使用的,BindingMethod是BindingMethods的值。
BindingAdapter用法:
@androidx.databinding.BindingAdapter({"url"})
public static void loadImage(ImageView view, String url) {
Log.i(TAG, "url:" + url);
Glide.with(view.getContext()).load(url).into(view);
}
这里又有一个自定义属性url,通过BindingAdapter注解,DataBinding最终会调用loadImage方法,然后我们在该方法里实现自己的逻辑,比如加载网络图片。
6. BindingConversion
这个就比较简单了,可以实现特定类型的自定义转换,二次处理,只要xml中的属性的参数和返回值与其返回值被BindingConversion注解的方法的参数和返回值一值,就会调用该方法,拿到返回值后再进行设置。
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@{color}"/>
@androidx.databinding.BindingConversion
public static Drawable convertStringToDrawable(int color) {
return new ColorDrawable(color);
}
7. 双向数据绑定
上面所说的都是单向绑定,也就是从data到UI,但DataBinding也是支持从UI到data的,比如输入框内容发生改变了,data的值也会跟着改变,SeekBar的进度发生改变了,data的值也跟着改变,为了实现从UI到data的绑定,需要使用@={}符号:
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={data.text}"
android:layout_marginTop="20dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{data.text}"/>
可以看到,EditText和data.text实现了双向绑定,data.text的值发生了改变,会显示到EditText上,在EditText上直接修改文本,也会导致data.text变成修改后的文本,下面的进度条也是类似:
<SeekBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progress="@={data.progress}"
android:layout_marginTop="50dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(data.progress)}"
android:layout_marginTop="20dp"
android:textSize="20sp"/>
7. include和ViewStub
DataBinding支持对include进行数据绑定,值会被传到include实际的布局中,通过也支持ViewStub。
三、原理
DataBinding的实现原理不算复杂,通过反编译生成的apk,或者调试内部代码,大概就会明白其中的实现方法,可以分为两个阶段,编译阶段和运行阶段。
编译阶段
在编译时,DataBinding编译器会处理layout为根节点的xml文件,然后生成正常的xml布局文件,还是最开始的demo,生成的xml布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:tag="layout/activity_basic_0"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:tag="binding_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"/>
</LinearLayout>
可以看到,这个多了tag信息,这个是DataBinding自动加上的,待会在运行阶段会用到,会通过该tag找到该View对应的绑定的值。
编译时DataBinding还会生成几个类,最关键的就是XXXBindingImpl类,这个类的类名默认是根据xml命名按照一定规律生成的,当然也可以在data标签下指定为其它名字,这个XXXBindingImpl类就是实现data和UI绑定的关键,在运行阶段会用到。
运行阶段
在运行时,我们会调用如下代码:
final ActivityBasicBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_basic);
binding.setText("hello world");
这里的ActivityBasicBinding对象,其实就是ActivityBasicBindingImpl类对象,它会做几件事:
在对象初始化时,寻找所有的View引用,然后保存起来。
在data发送改变时,会用Choreographer发送一个回调任务,该任务会在UI线程中执行。
上面所说的回调任务,就是执行UI刷新操作,最终会调用到ActivityBasicBindingImpl类的executeBindings方法,该方法会调用属性的对应的方法进行UI刷新。
看下ActivityBasicBindingImpl类的executeBindings方法:@Override protected void executeBindings() { long dirtyFlags = 0; synchronized(this) { dirtyFlags = mDirtyFlags; mDirtyFlags = 0; } java.lang.String text = mText; if ((dirtyFlags & 0x3L) != 0) { } // batch finished if ((dirtyFlags & 0x3L) != 0) { // api target 1 androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, text); } }
这里直接调用mboundView1的setText也是可以的,但可能会导致死循环,所以实际上还是调用了TextViewBindingAdapter的setText方法,这个TextViewBindingAdapter是官方提供的,也就是TextView的BindingAdapter,在setText里做了新旧值相等判断,不相等才需要更新UI。
四、总结
经过上面的使用和分析,已经较全面地学习DataBinding的使用和内部原理,能够在项目中很轻松地写出DataBinding代码,接下来分析下DataBinding的优点和存在的问题。
优点
可以避免无用的刷新,实现高效局部刷新,比如ListView或者RecyclerView某个Item的某个控件的刷新。
避免重复调用findViewById,DataBinding内部帮我们调用了,而且只调用一次。
能够实现MVVM模式,数据改变直接触发UI更新,VM不会持有View的引用,也不会做更新UI的逻辑,该开发模式同时也能节省不少代码,提高开发效率。
缺点
因为都是全自动绑定,出问题不好定位,调试不容易。
可能会导致占用内存大。
编译速度变慢。
适用场景
常规数据绑定,逻辑解耦。
实现局部刷新,提高性能。
加上特殊逻辑,比如ImageView加载图片时特殊逻辑。