前言
在以前的android开发中,布局文件通常只负责 UI控件的布局工作。页面通过 setContentView()方法关联布局文件,再通过 UI控件的 id 找到控件,接着在页面中通过代码对控件进行操作。相信,上面这几个步骤是雷打不动的模板代码。可以说,页面承担了绝大部分的工作量,为了减轻页面的工作量,Google在 2015年的I/O 大会上提出了 DataBinding。 DataBinding 的出现让布局文件承担了部分原本属于页面的工作,也使页面与布局文件之间的耩合度进一步降低。
DataBinding 具有如下优势:
- 项目更简洁,可读性更高。部分与UI控件相关的代码可以在布局文件中完成。
- 页面不再需要 findViewByid()方法。
- 布局文件可以包含简单的业务逻辑。UI控件能够直接与数据模型中的字段绑定,甚至能响应用户的交互。
其实,DataBinding 和 MVVM 架构是分不开的。实际上,DataBinding 是 Google 为了Android 能够更好地实现 MVVM 架构而设计的。因此,我们必须知道DataBinding的用法,如果我们不想成为普通的开发工程师,那么研究其原理则是我们必须要做的功课。
从上面的论述中,我们知道databinding大大减少了开发者的工作量,这说明了框架帮我们完成了大量的工作,而完成这些工作所需的代码毫无疑问是通过APT技术完成的。
除此之外,DataBinding有一个瑕不掩瑜的点——双向绑定。什么是双向绑定呢?一般情况,我们改变了数据,那么显示数据的界面通常会发生改变,这叫做单向绑定。而双向绑定,则增加了——界面内容的改变同步更新到数据上。那么它的缺点是什么呢?那就是太耗性能,别看我们自己写的代码量很少,但是它背后帮我们生成了成吨的代码,后面我们分析源码的时候就知道了。
简单用法
- 定义自己需要的Model类,继承BaseObservable类,如下面的User类:
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
// Model
public class User extends BaseObservable {
private String name;
private String pwd;
public User(String name, String pwd) {
this.name = name;
this.pwd = pwd;
}
@Bindable // BR里面标记生成 name数值标记
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name); // APT又是主接处理器技术 BR文件
}
@Bindable // BR里面标记生成 pwd数值标记
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
notifyPropertyChanged(BR.pwd); // APT又是主接处理器技术 BR文件,如果没有这句代码,那么就无法实现数据改变,界面也会跟着变化的功能
}
}
- 页面
public class MainActivity extends AppCompatActivity {
User user;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
user = new User("Brett", "123");
binding.setUser(user); // 必须要建立绑定关系,否则没有任何效果
//模拟服务器
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
user.setName(user.getName() + "哈");
user.setPwd(user.getPwd() + "哈");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
- 布局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.example.User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
android:textSize="50sp" />
<EditText
android:id="@+id/tv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.pwd}"
android:textSize="50sp" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={user.name}"
//加入等号意味着改变该EditText那么第一个EditText的显示内容也会改变,
//实现了ui改变==>数据改变的概念
android:textSize="50sp" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={user.pwd}"
android:textSize="50sp" />
</LinearLayout>
</layout>
这样,每过1s,我们更改user的数据时,EditText的显示内容也同时会发生改变。
上面,我们是使用Java语言来实现的,如果用kotlin语言该怎么实现呢?其实,都一样的,唯一的区别就在与model的写法。
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import androidx.databinding.ObservableField
class StudentInfo {
val nameF : ObservableField<String> by lazy { ObservableField<String>() }
val pwdF : ObservableField<String> by lazy { ObservableField<String>() }
}
如果使用kotlin语言,则必须使用ObservableField类包装,java语言的用法失效了。
DataBinding的简单用法就介绍到这里,当然还有一些进阶的用法,像如何将数据传入xml布局的layout标签里面,自定义适配器等。这些已经有一些博客说的很明白了,大家可以搜索一下。我们的重点在于分析DataBinding的实现原理,掌握核心技术😁😁😁
原理分析
老规矩,我们在进行原理分析的时候,肯定需要一个疑问。笔者在使用DataBinding的时候就有下面几个疑问:
- xml布局文件有点奇怪,按照我们的理解android的View在绘制扫描xml文件的时候它是如何区分data标签的?通俗地说,View应该是不认识data标签的,它们应该只认识TextView这些控件的。
- DataBinding是如何实现绑定的。
接下来,我们就带着上面两个疑惑来看源码。那我们改该从哪里看起呢?自然是从DataBindingUtil.setContentView方法看起了,因为这是我们唯一的入口点。我们点进去看下究竟做了什么事情。
看到没,怪不得我们不需要在Activity中调用setContentView方法,原来源码已经帮我们调用了。接下来,我们看下bindToAddedViews方法做了什么事情。
记住,看源码的法则——盯着入参。从源码中可以知道,如果是走if语句还是else语句都来到了bind方法。
bind方法没什么东西就只有一个sMapper,那这个是什么东西呢?是一个DataBinderMapper类型的变量。DataBinderMapper是一个抽象类,很显然,我们需要知道sMapper究竟是实现了DataBinderMapper哪个子类。
因此,我们进入DataBinderMapperImpl类看下。这。。。。怎么是null。是不是我们的想法有点问题,笔者也是在这里被坑了半天。
后来笔者采用了一个笨方法——全局搜DataBinderMapperImpl。发现居然有两个DataBinderMapperImpl文件一个是aar包里面的,另一个则是在编译生成的build文件夹中。大体知道是什么回事了。原来google通过apt技术生成了一个DataBinderMapperImpl类,我们应该要去编译生成的DataBinderMapperImpl类中查看源码才对。
看逻辑,好像是从view参数(就是布局文件的root)中拿取tag,如果tag等于layout/activity_main_0就new出一个ActivityMainBindingImpl对象。
看到这里,想必大家有下面几个疑惑:
- layout/activity_main_0这个是什么东西。
- tag又是什么时候被赋值的,view参数其实就是我们布局文件的root,记得我们并没有在布局文件中设置tag的
我们先不要急,悬念留在最后。我们继续看下去。接着我们进入ActivityMainBindingImpl类一看究竟。
public ActivityMainBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
this(bindingComponent, root, mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds));
}
private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 1
);
//为什么bindings的长度是5呢,因为我们在布局文件中的设置了5个控件嘛
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);
this.mboundView3 = (android.widget.EditText) bindings[3];
this.mboundView3.setTag(null);
this.mboundView4 = (android.widget.EditText) bindings[4];
this.mboundView4.setTag(null);
setRootTag(root);
// listeners
//重点关注
invalidateAll();
}
从上述代码中可以知道mapBindings返回来了一个bindings数组。因此,我们需要看下mapBindings方法做了什么事情。mapBindings很长这里就不带大家看了,大家看的时候牢记一个原则——盯着入参。这里就直接说下:mapBindings方法其实就是将我们布局文件中的一个个view控件赋值给bindings数组。接下来,我们要重点关注invalidateAll方法,因为这个方法最后将数据与view绑定在一起。
这个runnable接口,最后会调到ActivityMainBindingImpl(通过apt技术生成的那个)executeBindings方法,该方法就实现了数据与view的绑定操作。
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String userName = null;
com.derry.databinding_java.User user = mUser;
java.lang.String userPwd = null;
if ((dirtyFlags & 0xfL) != 0) {
if ((dirtyFlags & 0xbL) != 0) {
if (user != null) {
// read user.name
userName = user.getName();
}
}
if ((dirtyFlags & 0xdL) != 0) {
if (user != null) {
// read user.pwd
userPwd = user.getPwd();
}
}
}
// batch finished
if ((dirtyFlags & 0xbL) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, userName);
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView3, userName);
}
if ((dirtyFlags & 0xdL) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, userPwd);
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView4, userPwd);
}
if ((dirtyFlags & 0x8L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView4, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView4androidTextAttrChanged);
}
}
接下来,我们来解答上面2个疑惑。其实,DataBinding在编译期间会将我们原本的布局文件拆分成两份。这下我们明白了,View在绘制期间读取的布局文件是没有data标签的,还是我们以前那种只有控件的那种形式。
之前,我们在看源码时,不是有看到许多tag吗?原来是这里被赋值了的。
有读者可能会问——你是怎么知道布局文件被拆分成两份的。其实,笔者用了个笨方法:查找build文件夹。我们知道编译期间生成的文件都在这个文件夹中。
接下来,我们来解答最后一个疑惑——DataBinding是如何实现绑定的。那我们的入手点自然是Activity的setUser方法了。我们点击setUser方法,发现跳到了我们的布局文件去了。其实,我们应该看ActivityMainBindingImpl类才对的。前面,我们分析过的——DataBindingUtil.setContentView方法最后是返回了ActivityMainBindingImpl类。
我们要注意两个点,第一个是notifyPropertyChanged方法,第二个是super.requestRebind()方法。第二个方法是调到了ViewDataBinding类去了,这个方法我们之前分析过,它最终会将数据赋值到对应的view上。因此,接下来我们重点分析notifyPropertyChanged方法。这个方法,还记得我们在哪里见过吗?没错在我们自建的model类中,当我们调用set方法时,需要手动调用这个方法,如果不调用这个方法是无法实现数据绑定的。那么DataBinding实现数据绑定的奥秘也许就藏在这里面哦!!!
又是这种代码,如果有看过笔者前面几篇博客的读者就会自动,mCallbacks百分百是一个接口或者抽象类,我们要找到它的真正类型——PropertyChangeRegistry。
这里笔者就直接说了——notifyCallbacks。这个方法是我们需要关注的方法。notifyRemainder这个方法最后也是会调到notifyCallbacks方法来的。
现在,我们的疑问就在于要确定下callback的类型等价于确定mCallbacks集合里面存放了什么类型的的元素。现在我们来看下mCallbacks是什么时候被赋值的。还记不记得我们在分析setUser方法的时候,有一个updateRegistration方法没有分析到。接下来,我们继续分析updateRegistration方法。
这里需要注意,我们的WeakListener的实现类是WeakPropertyListener类型,里面有一个listener是WeakListener类。
addListener是调到了WeakPropertyListener类里面的。
哦!原来mCallbacks是这么被赋值的,同时我们也明白了mCallbacks存储的元素是WeakPropertyListener类型。所以,前面的onNotifyCallback方法调用的onPropertyChanged方法是在WeakPropertyListener类中的。说实话,正佩服Google大大,封装得正特么深啊!
这个requestRebind方法就不用说了吧!里面就是将view和数据绑定在一起的。前面已经出现了好几次了。
至此,我们解答了开篇提出的几个疑惑。感谢各位的观看!!!💕💕💕