当前位置:网站首页>DataBinding使用与原理分析
DataBinding使用与原理分析
2022-06-26 09:46:00 【hzulwy】
前言
在以前的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和数据绑定在一起的。前面已经出现了好几次了。
至此,我们解答了开篇提出的几个疑惑。感谢各位的观看!!!
边栏推荐
- 8- creating leecode algorithm with pictures and texts - algorithm solution of minimum stack and LRU caching mechanism
- Global and Chinese markets of children's electronic thermometers 2022-2028: Research Report on technology, participants, trends, market size and share
- Cmake / set command
- Développeur, quelle est l'architecture des microservices?
- SwiftUI 开发经验之为离线优先的应用程序设计数据层
- 【Leetcode】76. Minimum covering substring
- Global and Chinese market of aluminum sunshade systems 2022-2028: Research Report on technology, participants, trends, market size and share
- 36 qtextedit control input multiline text
- Small example of SSM project, detailed tutorial of SSM integration
- Win10安装tensorflow-quantum过程详解
猜你喜欢

Allocation of heap memory when creating objects

MySQL seventh job - update data

Under the double reduction, the amount of online education has plummeted. Share 12 interesting uses of webrtc

MySQL 13th job - transaction management

MySQL第十三次作业-事务管理

Basic MySQL

MySQL第六次作业-查询数据-多条件

OpenCV图像处理-灰度处理

String constant pool, class constant pool, and runtime constant pool

【LeetCode】59. 螺旋矩阵 II
随机推荐
Linux下安裝Mysql【詳細】
【LeetCode】59. Spiral matrix II
36 qtextedit control input multiline text
Progressive Web 应用程序PWA是应用程序开发的未来
MySQL第九次作业-连接查询&子查询
Procedure Call Standard
Appium automation test foundation - mobile end test environment construction (II)
Express (I) - easy to get started
Little red book - Summary of internal sales spike project
Leetcode intermediate node of linked list
Problems encountered in the application and development of Hongmeng and some roast
Write data to local file
MySQL第六章总结
Blog article index summary -- Software Engineering
Execute Lua script in redis
【無標題】
Yarn package management tool
The IE mode tab of Microsoft edge browser is stuck, which has been fixed by rolling back the update
About multi table query of MySQL
MySQL第七次作业-更新数据