DataBinding使用与原理分析

前言

在以前的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的时候就有下面几个疑问:

  1. xml布局文件有点奇怪,按照我们的理解android的View在绘制扫描xml文件的时候它是如何区分data标签的?通俗地说,View应该是不认识data标签的,它们应该只认识TextView这些控件的。
  2. 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对象。
看到这里,想必大家有下面几个疑惑:

  1. layout/activity_main_0这个是什么东西。
  2. 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类里面的。
![在这里插入图片描述](https://img-blog.csdnimg.cn/8c5017826b204b65a14a05e7b30e100a.png
在这里插入图片描述

在这里插入图片描述
哦!原来mCallbacks是这么被赋值的,同时我们也明白了mCallbacks存储的元素是WeakPropertyListener类型。所以,前面的onNotifyCallback方法调用的onPropertyChanged方法是在WeakPropertyListener类中的。说实话,正佩服Google大大,封装得正特么深啊!
在这里插入图片描述
在这里插入图片描述
这个requestRebind方法就不用说了吧!里面就是将view和数据绑定在一起的。前面已经出现了好几次了。

至此,我们解答了开篇提出的几个疑惑。感谢各位的观看!!!💕💕💕

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值