DataBinding的使用与原理

A man can be destroyed but not defeated. —— Daily English

DataBinding是一种工具,能在编译时绑定布局和对象。通过这篇文章,一是要掌握DataBinding的使用,二是我们要弄懂,View层是怎么改变Model的,而Model层又是如何改变View的。

介绍

APT预编译方式

我们已经知道,DataBinding里面的功能类是通过APT的工具来产生的。

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。简单来说就是在编译期,通过注解生成.java文件。

当你的xml用DataBinding规定的格式去书写的时候,DataBinding就能够通过APT的技术,帮你生成对应的类文件。

Rebuild完成后,会在build目录下生成以[布局名]Binding.java文件,这里我们的布局叫activity_main,所以生成了一个名为ActivityMainBinding.Java类文件,以及继承自它的ActivityMainBindingImpl.java类文件。

生成文件的具体位置参考下图

在这里插入图片描述

布局的格式和处理

我们需要规定的格式来定义我们的xml布局文件,参考布局activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- DataBinding编码规范 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 定义该View(布局)需要绑定的数据来源 -->
    <data>
        <variable
            name="user"
            type="pers.owen.databinding.model.UserInfo" />
    </data>

    <!-- 布局常规编码 -->
    <LinearLayout...>
        <EditText ... />
        <EditText ... />
    </LinearLayout>
</layout>

<data>标签中绑定的可以是一个实体类,也可以是一个ViewModel,只要按照规定的格式去写,就可以。

在编译时,DataBinding会内部处理布局文件控件,重新生成两个全新的布局文件。一个与原文件同名,叫activity_main.xml,另一个是名为activity_main-layout.xml

生成文件的具体位置参考下图
在这里插入图片描述
生成后的activity_main.xml就跟我们普通的xml文件一样,用于Android os的渲染。但是DataBinding会在每个控件上都加上Tag,这个Tag就是用来快速查找和定位具体控件的。

生成后的activity_main.xml代码如下

<?xml version="1.0" encoding="utf-8"?>
<!-- DataBinding编码规范 -->
                                                                   
    <!-- 定义该View(布局)需要绑定的数据来源 -->
    
                 
                       
                                                          
           

    <!-- 布局常规编码 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical" android:tag="layout/activity_main_0" xmlns:android="http://schemas.android.com/apk/res/android">

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:tag="binding_1"      />

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:tag="binding_2"     />
    </LinearLayout>
         

生成的activity_main-layout.xml文件,是DataBinding生成的配置文件,它能够通过文件中的配置信息,快速定位哪一个控件,该显示什么信息。

activity_main-layout.xml代码如下

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="C:\workspace\adrproject\MyDataBinding\app\src\main\res\layout\activity_main.xml"
    isBindingData="true"
    isMerge="false" layout="activity_main" modulePackage="pers.owen.databinding"
    rootNodeType="android.widget.LinearLayout">
    <Variables name="user" declared="true" type="pers.owen.databinding.model.UserInfo">
        <location endLine="7" endOffset="57" startLine="5" startOffset="8" />
    </Variables>
    <Targets>
        <Target tag="layout/activity_main_0" view="LinearLayout">
            <Expressions />
            <location endLine="27" endOffset="18" startLine="11" startOffset="4" />
        </Target>
        <Target tag="binding_1" view="EditText">
            <Expressions>
                <Expression attribute="android:text" text="user.name">
                    <Location endLine="20" endOffset="39" startLine="20" startOffset="12" />
                    <TwoWay>true</TwoWay>
                    <ValueLocation endLine="20" endOffset="37" startLine="20" startOffset="29" />
                </Expression>
            </Expressions>
            <location endLine="20" endOffset="42" startLine="17" startOffset="8" />
        </Target>
        <Target tag="binding_2" view="EditText">
            <Expressions>
                <Expression attribute="android:text" text="user.pwd">
                    <Location endLine="26" endOffset="38" startLine="26" startOffset="12" />
                    <TwoWay>true</TwoWay>
                    <ValueLocation endLine="26" endOffset="36" startLine="26" startOffset="29" />
                </Expression>
            </Expressions>
            <location endLine="26" endOffset="41" startLine="22" startOffset="8" />
        </Target>
    </Targets>
</Layout>

布局这块,我们需要做的就是按照DataBinding要求的格式,定义好布局即可,其他都会在Rebuild的时候,自动生成。

关联Activity组件与布局

我们通过代码DataBindingUtil.setContentView(Activity,Layout);来关联Activity组件与布局。有人可能会疑问,为什么不直接在Activity中setCentView。那是因为DataBinding需要Activity来获取根布局,到时候View层刷新后会通过根布局来查找并更新相应的控件。

何时生成设置Model帮助类?

在Rebuild的时候DataBinding会扫描所有Module中的xml文件。只有当你的xml布局中有符合DataBinding定义规范的<data>标签,才会去生成对应的帮助类。

DataBinding TestActivity.java <–> activity_ databinding_test.xml --> ActivityDatabindingTestBinding --> ViewDataBinding


实战

我们这边写一个小Demo,实现DataBinding的双向数据绑定即可。

引入

在module的build.gradle文件中引入DataBinding

android {
	...
	// 添加DataBinding依赖
	dataBinding{
	    enabled = true
	}
)

定义实体类

定义实体类,可以定义成原始的属性格式,也可以定义成被观察者属性格式。

我们先试一下定义成原始属性格式的实体类,也就是我们之前定义实体类的格式。

public class UserInfo extends BaseObservable {
    private String name;
    private String pwd;

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
        notifyPropertyChanged(BR.pwd);
    }
}

注意三点,1、继承BaseObservable类,2、get()方法上加上@Bindable注解,3、set()方法中加入notifyPropertyChanged

定义Layout布局
页面很简单就不截图了,两个EditText,分别对应UserInfo中的name和pwd。

<?xml version="1.0" encoding="utf-8"?>
<!-- DataBinding编码规范 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 定义该View(布局)需要绑定的数据来源 -->
    <data>
        <variable
            name="user"
            type="pers.owen.databinding.model.UserInfo" />
    </data>

    <!-- 布局常规编码 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={user.name}" />

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:text="@={user.pwd}" />
    </LinearLayout>
</layout>

Rebuild Project

Rebuild完成后,就会ActivityMainBinding.Java文件和ActivityMainBindingImpl.java文件,生成目录可参看本文开头部分。

书写代码绑定

public class MainActivity extends AppCompatActivity {
    private UserInfo userInfo = new UserInfo();
    private final static String TAG = "TAG >>>";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        // 尝试1:单向绑定第一种方式:<Model -- View>
        userInfo.setName("Owen");
        userInfo.setPwd("123");
        binding.setUser(userInfo);

        Log.e(TAG, userInfo.getName() + " / " + userInfo.getPwd());
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                userInfo.setName("Sara");
                userInfo.setPwd("222");
                Log.e(TAG, userInfo.getName() + " / " + userInfo.getPwd());
            }
        }, 2000);

    }
}

通过DataBindingUtil.setContentView(this, R.layout.activity_main)绑定布局,获取到ActivityMainBinding对象binding,再通过binding.setUser(userInfo)绑定实体类。

我们做了尝试1,给UserInfo的Name和Pwd设值,用Handler延时2s后重新改变model的属性值,来查看与之绑定的View是不是会发生变化。效果图如下:

在这里插入图片描述
我们看到结果是符合我们预期的,验证了Model层JavaBean中的属性值改变,会更新到View层的控件上。

定义被观察属性的实体类

我们前面说过了,实体类的定义还有第二种方式,格式如下:

public class UserInfo {
    public ObservableField<String> name = new ObservableField<>();
    public ObservableField<String> pwd = new ObservableField<>();
}

显然,这种方式直接把属性定义成ObservableField<T>,这是DataBinding为我们提供的,大大减少了实体类定义的复杂度。

定义实体类的时候,我们需要记住两点规则:一个是有被观察者属性,第二个就是有刷新属性的方法

在原生的实体类中,我们需要自己去继承,去加注解,调用notifyPropertyChanged去实现。而DataBinding为我们提供的ObservableField<T>本身就是一个观察者属性。

这种定义方式对实体类属性有特定的取值和赋值方式,我们来看第二个尝试

public class MainActivity extends AppCompatActivity {
    private UserInfo userInfo = new UserInfo();
    private final static String TAG = "TAG >>>";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        // 尝试1:单向绑定第一种方式:<Model -- View>
        ...

        // 尝试2:单向绑定第二种方式:<Model -- View>
        userInfo.name.set("Owen");
        userInfo.pwd.set("123");
        binding.setUser(userInfo);
        Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                userInfo.name.set("Sara");
                userInfo.pwd.set("222");
                Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
            }
        }, 2000);
    }
}

运行效果和尝试1完全一致。

双向的数据绑定

以上都是Model层的数据改变,更新View层的显示,也就是所谓的单向绑定。我们来做第三个尝试,测试数据的双向绑定,不仅看Model–>View 的更新,也来看一下View层数据的改变,是不是也能更新其绑定Model中属性的值。

public class MainActivity extends AppCompatActivity {
    private UserInfo userInfo = new UserInfo();
    private final static String TAG = "TAG >>>";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        // 尝试1:单向绑定第一种方式:<Model -- View>
        ...

        // 尝试2:单向绑定第二种方式:<Model -- View>
		...

        // 尝试3:双向绑定(Model --- View     View  --- Model)
        userInfo.name.set("Owen");
        userInfo.pwd.set("123");
        binding.setUser(userInfo);

        Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.e(TAG, userInfo.name.get() + " / " + userInfo.pwd.get());
            }
        }, 6000);
    }
}

我们先给UserInfo,设置Name和Pwd,检测是否会更新到View上。然后利用Handler延迟6s后重新读取UserInfo的属性。在这6s内,我们需要在界面上修改Name和Pwd对应的EditText值,来进行验证。

效果如下

在这里插入图片描述
logcat控制台输出
在这里插入图片描述
这个结果是符合预期的,借助DataBinding成功实现了双向的数据绑定。

如果View到Model更新失败的同学,可以参考排查xml布局文件中,android:text="@={user.name}"@后面的=是否有加上。

文中Demo会在文末给出。


源码分析

绑定过程

我们从DataBindingUtil.setContentView(this, R.layout.activity_main)方法开始,跟踪这个绑定的过程。

	ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
	
	↓
	
	public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
        	int layoutId) {
   		 return setContentView(activity, layoutId, sDefaultComponent);
	}

	↓

    public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
    	...
        return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
    }
    
	↓
	
    private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
		...
        return bind(component, childView, layoutId);
    }
    
    ↓
    
    static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId);
    }

绑定的过程最后跟踪到了sMapper.getDataBinder(bindingComponent, roots, layoutId)方法,我们来看这个方法是如何实现的。查找该方法的实现。

  @Override
  public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
	...
    if ("layout/activity_main_0".equals(tag)) {
       return new ActivityMainBindingImpl(component, view);
     }
    ...
  }

activity_main_0是不是很熟悉。我们已经知道,在Rebuild项目的时候,我们xml文件会被重新生成两个xml文件。生成后的activity_main.xml中所有的控件,都被添加了tag属性,而最外层layout的tag就是用[布局名]_0组成的。可参考本文章前段部分关于生成新xml的代码。

所以当找到我们常规的布局文件后,就开始new ActivityMainBindingImpl(component, view)创建DataBinding的功能类,开始搞事情。

我们来看ActivityMainBindingImpl类构造方法的实现。

    private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        super(bindingComponent, root, 2
            );
        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);
        setRootTag(root);
        // listeners
        invalidateAll();
    }

标记1。我们可以看到这里是有一个Object[] bindings数组来记录我们布局文件中的三个控件。分别设置给了类中的三个成员变量mboundView0mboundView1mboundView2

然后执行invalidateAll(),我们对该方法做一个跟踪。

    @Override
    public void invalidateAll() {
        ....
        requestRebind();
    }
        
	↓
	
    protected void requestRebind() {
		....
        mUIThreadHandler.post(mRebindRunnable);
    }

标记2。我们看到,DataBinding会定义一个Handler去执行mRebindRunnable,这个mRebindRunnable里面也就是我们V和M双向绑定的核心核能。我们来看这个mRebindRunnable是如何实现的。

    private final Runnable mRebindRunnable = new Runnable() {
            ...
            executePendingBindings();
    };
        
	↓
	
    public void executePendingBindings() {
    		...
            executeBindingsInternal();
    }
        
	↓
	
   private void executeBindingsInternal() {
			...
            executeBindings();
			...
    }

mRebindRunnable最终会执行到executeBindings()方法。executeBindings()方法是在ActivityMainBindingImpl中实现的,代码如下。

    @Override
    protected void executeBindings() {
        ...
        androidx.databinding.ObservableField<java.lang.String> userName = null;
        java.lang.String userPwdGet = null;
        pers.owen.databinding.model.UserInfo user = mUser;
        java.lang.String userNameGet = null;
        androidx.databinding.ObservableField<java.lang.String> userPwd = null;

        ...
        userNameGet = userName.get();
		...
        userPwdGet = userPwd.get();
		...
        androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, userNameGet);
		...
        androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView1, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView1androidTextAttrChanged);
        androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView2, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView2androidTextAttrChanged);
		...
        androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, userPwdGet);
		...
    }

最后这四行代码,短的这两行是setText(),作用是把从JavaBean属性中读出来的值,设置到View上。也就是实现了Model到View更新。长的这两行是setTextWatcher(),作用可想而知,就是为View控件设置监听,以改变对应JavaBean的属性的值。这样也就实现了View到Model的更新。

我们来看这两个监听中其中一个mboundView1androidTextAttrChanged是如何定义的。

    private androidx.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new androidx.databinding.InverseBindingListener() {
        @Override
        public void onChange() {
			...
            java.lang.String callbackArg_0 = androidx.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
			...
            userName.set(((java.lang.String) (callbackArg_0)));
		    ...
    };

把多余的代码去掉,逻辑不要太清晰。

至此,我们就完全弄懂了DataBinding的绑定过程。知道了,View层是怎么改变Model的,而Model层又是如何改变View的,这个数据双向绑定过程。

整个过程中,没有反射!

那这个mRebindRunnable又是何时被执行的呢,换句话说,我们这个双向数据的更新动作是什么时候执行的呢?

我们找到mRebindRunnable调用的位置,在ViewDataBinding的静态代码块,会有一个全局的监听,这个监听也就是用来通知数据更新的。

    static {
        if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
            ROOT_REATTACHED_LISTENER = null;
        } else {
            ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
                @TargetApi(VERSION_CODES.KITKAT)
                @Override
                public void onViewAttachedToWindow(View v) {
                    // execute the pending bindings.
                    final ViewDataBinding binding = getBinding(v);
                    binding.mRebindRunnable.run();
                    v.removeOnAttachStateChangeListener(this);
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                }
            };
        }
    }

标记3。每当View改变的时候,这个监听中的回调方法就会被执行,通过一个Handler去post我们的mRebindRunnable,从而更新Model。反之亦然。

至此,绑定过程分析完毕。

内存消耗简析

我在绑定过程中标记了三个标记,想必大家都注意到了。这三个标记就是需要额外消耗内存的地方。

标记1:额外数组
定义了一个额外的数组来记录这些控件。数组造成的额外内存开销。

标记2:Runable
只要当控件发生任何的数据改变,它都有一个监听。在这个监听中,Binder会new一个Runable。
每个Activity都会有一个Runable,10个Activity就会创建10个Runable。

标记3:handler的loop一直在等待状态。
一旦Model的数据刷新,最后都是通过Handler去刷新UI的。我们知道Handler中有一个管道,会一直等待消息进来,然后通过Lopper.loop()方法,去取消息处理。当有消息了,就通过这个Tag找到控件,为控件赋值。Handler的等待造成的额外内存开销。

本文完!相信坚持看完本文的同学一定会有收获。


文中Demo下载地址

Android 架构设计模式系列文章索引

MVC架构设计与经典的三层模型

MVVM实现数据双向绑定

DataBinding的使用与原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章