[转] DataBinding 数据绑定

数据绑定分为单项绑定和双向绑定两种。单向绑定上,数据的流向是单方面的,只能从代码流向 UI;双向绑定的数据是双向的,当业务代码中的数据变化是,UI 上的数据能够得到刷新;当用户通过 UI 交互编辑了数据时,数据的变化也能自动的更新到业务代码中的数据上。

Android DataBinding Framework

在 2015 年的谷歌 IO 大会上,Android UI Toolkit 团队发布了 DataBinding 框架,将数据绑定引入了 Android 开发,当时还只支持单向绑定,而且需要作为第三方依赖引入,时隔一年,双向绑定这个特性也得到支持,同时纳入了 Android Gradle Plugin(1.5.0+) 中,只需要在 gradle 配置文件中添加三行代码,就能用上数据绑定。

android {
    dataBinding {
        enabled true
    }
}

使用数据绑定的优点

  1. 能有效提高开发效率,减少大量需要手动编写的胶水代码(如 findViewByIdsetOnClickListener)
  2. 高性能(绝大部分的工作在编译器完成,避免运行时使用反射)
  3. 使用灵活(可以使用表达式在布局内进行一定的逻辑运算)
  4. 具有 IDE 支持(语法高亮、自动补全,语法错误标记)

举个简单的例子

需求:界面上有两个控件,EditText 用于获取用户输入,TextView 用于把用户输入展示出来。

传统实现:用传统的方式来实现,需要定义一个布局,设置好这两个控件,然后再代码中引用这个布局,把这两个控件找出来,然后添加监听器到 EditText 上,在输入发生变化的时候,获取输入,然后更新到 TextView 上。

而是用数据绑定,代码如下:



    
        
    

    
        
        

        
        

    


class MainAct : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ContentMainBinding = ContentMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

}
图 1 动态绑定效果图

使用了数据绑定,代码逻辑结构变得清晰,手动编写的胶水代码得到了简化(有数据绑定框架替我们生成),数据绑定框架帮我们做了控件的数据变化监听,并将数据同步更新到控件上。

注意 如果是使用 Kotlin,需要在 app 的 build.gradle 文件中添加如下文件:

apply plugin: 'kotlin-kapt'
...

dependencies {
    ...
    kapt "com.android.databinding:compiler:3.1.2"
}

数据绑定的使用

布局文件的改造

使用数据绑定的布局文件以 标签作为根节点,表明这是个数据绑定的布局,修改后数据绑定框架会生成对应的 *Binding 类,如 content_main.xml 会生成 ContentMainBinding 类,即默认规则是:单词首字母大写,移除下划线,并在最后添加上 Binding。

数据的声明和辅助类的声明

标签内部添加 标签,即可声明数据。给 标签添加 class 属性可以改变生成的 *Binding 类的名字,如使用 将其改为 ContentMain
数据标签内部通过 标签声明变量,通过 标签导入辅助类,为了避免同名冲突,可以使用 alias 属性指定一个别名。


    
    

数据绑定的使用

变量声明之后,就可以在布局中使用了,使用的方式和使用 Java 类似,当表达式使用一个对象内的属性时,会分别尝试直接调用、getter、ObservableField.get()。
数据绑定内支持表达式,可以使用表达式来进行一些基本的逻辑运算。
常用的操作有:

  1. 数学计算福:+、-、*、/、%
  2. 字符串拼接:+
  3. 逻辑运算符:&&、||
  4. 比较运算符:==、>、<、>=、<=
  5. 函数调用
  6. 类型转换
  7. 数据存取[],对容器类的操作支持使用这种方式来存取
  8. Null 合并运算符:??,合并运算符会在变量非空的时候使用左边的操作,反之使用右边的,
    data ?? data.defaultVal

事件绑定

严格意义上来说,事件绑定也属于数据绑定的一种。之前我们常在布局内进行的 android:onClick="onBtnClick" 就可以视作一种数据绑定。但通过使用数据绑定框架,允许做更多事情。
可以通过数据绑定,传入一个变量,调用该变量上的方法用于事件的处理,跟原有的方式对比,数据绑定允许我们将处理事件的逻辑和布局所关联的类解耦,可以方便的替换不同的处理逻辑。
也可以通过表达式,在布局内直接执行一些代码,不需要我们切换回 Java 代码中去实现,对于一些不需要外部处理,仅仅是布局内相关的逻辑来说,这种特性允许我们把 UI 相关的逻辑进行内聚。



    
        
        

        
        
    

    

        
        

数据绑定框架的另一个特性,在进行数据相关的操作前,会检查变量是否为空,倘若没有传入对应的变量,或者控件为空,在布局上进行的操作并不会执行,因此,加入上述例子中,我们没有传入对应的 presenter 对象,点击按钮并不会引发 Crash。
还有,由于编译期会进行检查,加入对应的数据类型上没有实现对应的方法,或方法签名不对(参数类型应为 View),那么编译的时候就会报错,代码的稳定性也因此得到了保障。

数据模型

虽然数据绑定支持的 POJO(Pure Old Java Object,普通 Java 类,值仅具有一部分 getter/setter 方法的类),但对 POJO 对象的数据更新并不会同步更新 UI。为了实现自动更新,可以选择:

  1. 继承自 BaseObservable,给 getter 加上 @Bindable 注解,并在 setter 中实现域的变动通知;
  2. 如果数据类无法继承 BaseObservable,变动通知可以用 PropertyChangeRegistry 来实现;
  3. 最后一种是使用 Observable 域,对数据存取通过 ObservableFieldgetset 方法调用实现。ObservableField 是泛型类,对于基础类型,有对应的 ObservableIntObservableLongObservableShort 等可供使用;另外对于容器,每次优惠更新其中一项,而不是整个更新,因此还有对应的 ObservableArrayListObservableArrayMap 可供使用。
    从使用上来说,第三种方式更加直观和便捷,需要人工接入的地方更少,更不容易出错,推荐使用。

数据绑定的原理

数据绑定相关类的初始化

首先需要找一个切入点,最显而易见的切入点便是 ContentMainBinding.inflate,这个类似数据绑定框架生成的,生成的文件位于 build/intermediates/classes/debug//databinding 目录下。

public static ContentMainBinding inflate(@NonNull android.view.LayoutInflater inflater) {
    return inflate(inflater, android.databinding.DataBindingUtil.getDefaultComponent());
}

方法的实现调用了另一个 inflate 方法,经过几次辗转,最终调用到了 ContentMainBinding.bind 方法。

public static ContentMainBinding bind(@NonNull android.view.View view, 
                                      @Nullable android.databinding.DataBindingComponent bindingComponent) {
    if (!"layout/content_main_0".equals(view.getTag())) {
        throw new RuntimeException("view tag isn't correct on view:" + view.getTag());
    }
    return new ContentMainBinding(bindingComponent, view);
}

这个方法首先检查这个 view 是否是数据绑定相关的布局,不是则会抛出异常,是的话则实例化 ContentMainBinding
ContentMainBinding 实例化代码如下:

public ContentTestBinding(@NonNull android.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
    super(bindingComponent, root, 0);
    final Object[] bindings = mapBindings(bindingComponent, root, 6, sIncludes, sViewsWithIds);
    this.fullName = (android.widget.TextView) bindings[5];
    this.mboundView0 = (android.widget.LinearLayout) bindings[0];
    this.mboundView0.setTag(null);
    this.mboundView1 = (android.widget.Button) bindings[1];
    this.mboundView1.setTag(null);
    this.mboundView2 = (android.widget.EditText) bindings[2];
    this.mboundView2.setTag(null);

    setRootTag(root);
    // listeners
    mCallback1 = new android.databinding.generated.callback.OnClickListener(this, 1);
    invalidateAll();
}

构造函数内首先调用 mapBindingsroot 中所有的 view 找出来,数字 3 指的是布局中总共有 8 个 view,然后还传入 sIncludessViewWithIds,前者是布局中 include 进来的布局的索引,后者是布局中包含 id 的索引。
这两个参数是静态变量,初始化如下:

@Nullable
private static final android.databinding.ViewDataBinding.IncludedLayouts sIncludes;
@Nullable
private static final android.util.SparseIntArray sViewsWithIds;
static {
    sIncludes = null;
    sViewsWithIds = new android.util.SparseIntArray();
    sViewsWithIds.put(R.id.fullName, 5);
}

由于 Demo 中的布局不包含 include,因此 sIncludes 被值为 null,而布局内有一个 id 为 R.id.fullName 的控件,因此他被加入到 sViewsWithIds 中,5 表示他在 bindings 中的索引。
再回到构造函数,mapBindings 查找到的 View 都防止在 bindings 这个数组中,并通过生成代码的方式,将它们一一取出来,转化为对应的数据类型,有设置 id 的控件,会以 id 作为变量名,没有设置 id 的控件,则以 mboundView + 数字 的方式依次赋值。然后将这个 Binding 和 肉体 关联起来(通过将 Binding 设为 rootView 的 tag 的方式)。
还实例化了一个 OnClickListener,用于绑定事件响应。
mapBindings 的方法实现在 ViewDataBinding 这个类里面,主要是把 root 内所有的 view 给查找出来,并放置到 bindings 对应的索引内,这个索引如何确定呢?原来,数据绑定在处理布局的时候,生成了辅助信息在 view 的 tag 里,通过解析这个 tag,就能知道对应的索引了。所以,为了避免自己 inflate 布局之后,不小心操作了 view 的 tag 对解析产生干扰,尽量使用数据绑定来得到 inflate 之后 的 view。处理过的布局片段如下,生成位置为 app/build/intermediates/data-binding-layout-out//layout/ 目录。
mapBindings 方法比较长,里面针对不同情况进行了处理,虽然这个方法看似使用了递归,但实际上是通过这种方式实现对 root 下所有的控件的遍历,因此整个方法的时间复杂度是 O(n),通过一次遍历,找到所有空间,整体性能比使用 findViewById 还优秀。
实例化的 OnClickListener 接受两个参数,一个是 OnClickListener.listenerContentMainBinding 实现了这个接口,所以第一个参数传的值是 ContentMainBinding,另一个是标识这个 listener 作用的控件的 sourceId。这个 OnClickListener 干的事情很简单,就是把点击事件,附加上 sourceId,回传给了 ContentMainBinding_internalCallbackOnClick 处理,也就是最后所有跟布局相关的操作逻辑内聚到了 ContentMainBinding 这个类中。

// callback impls
public final void _internalCallbackOnClick(int sourceId, android.view.View callbackArg_0) {
    // localize variables for thread safety
    // firstName
    java.lang.String firstName = mFirstName;
    // (firstName) + ('·')
    java.lang.String firstNameChar = null;
    // lastName
    java.lang.String lastName = mLastName;
    // ((firstName) + ('·')) + (lastName)
    java.lang.String firstNameCharLastName = null;
    
    if ((fullName) != (null)) {
        firstNameChar = (firstName) + ('·');
        firstNameCharLastName = (firstNameChar) + (lastName);
        fullName.setText(firstNameCharLastName);
    }
}

从实现可以看到,这里仅仅实现了在布局中写下的内部处理逻辑 () -> fullName.setText(firstName + lastName),由于布局中这样的处理逻辑仅有一处,所以这里 sourceId 没有使用到。如果有多于 2 处的逻辑,这里会生成一个 switch 块,通过 sourceId 执行不同的指令。从实现还可以看到,框架生成的代码使用本地变量来持有成员变量,以保证对变量的访问是线程安全的。同样的,在对访问控件之前,会进行是否为空的检查,避免空指针错误。这也是使用数据绑定的带来的好处:通过框架自动生成的代码中的为空检查,避免手工编码容易导致的空指针错误。
但是构造函数这里仅仅是创建了监听器,并没有将它 set 到对应的控件中去。


数据绑定的 Rebind 机制

在构造函数的最后,调用了方法 invalidateAll

@Override
public void invalidateAll() {
    synchronized(this) {
        mDirtyFlags = 0x8L;
    }
    requestRebind();
}

invalidateAll 方法将脏标记位 mDirtyFlags 标记为 0x8L,这个脏标记位是一个 long 的值,也就是最多有 64 个位可供使用。由于 mDirtyFlags 这个变量是成员变量,且多处会对其进行写操作,所以对他的写操作都是同步进行的。更新完这个值,紧接着就调用 requestRebind 方法,请求执行 rebind 操作。
这个方法的实现在 ContentMainBinding 的基类 ViewDataBinding 中。

protected void requestRebind() {
    if (mContainingBinding != null) {
        mContainingBinding.requestRebind();
    } else {
        synchronized (this) {
            if (mPendingRebind) {
                return;
            }
            mPendingRebind = true;
        }
        if (mLifecycleOwner != null) {
            Lifecycle.State state = mLifecycleOwner.getLifecycle().getCurrentState();
            if (!state.isAtLeast(Lifecycle.State.STARTED)) {
                return; // wait until lifecycle owner is started
            }
        }
        if (USE_CHOREOGRAPHER) {
            mChoreographer.postFrameCallback(mFrameCallback);
        } else {
            mUIThreadHandler.post(mRebindRunnable);
        }
    }
}

如果此前没有请求执行 rebind 操作,那么会将 mPendingRebind 置为 true,API 等级 16 及以上,会往 mChoreographer 发一个 mFrameCallback,在系统刷新界面 (doFrame) 的时候执行 rebind 操作,API 16 以下,则是往 UI 线程 post 一个 mRebindRunnable 任务。mFrameCallback 的内部实际上调用的是 mRebindRunnablerun 方法,因此这两个任务除了调用时机,干的事情其实没什么不同。
而如果此前请求过执行 rebind 操作,即已经 post 了一个任务到队列去,而且这个任务还未获得执行,此时 mPrendingRebind 的值为 true,那么 requestRebind 将直接返回,避免重复、频繁执行 rebind 操作带来的性能损耗。

private final Runnable mRebindRunnable = new Runnable() {
    @Override
    public void run() {
        synchronized (this) {
            mPendingRebind = false;
        }
        processReferenceQueue();

        if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
            // Nested so that we don't get a lint warning in IntelliJ
            if (!mRoot.isAttachedToWindow()) {
                // Don't execute the pending bindings until the View
                // is attached again.
                mRoot.removeOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
                mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
                return;
            }
        }
        executePendingBindings();
    }
};

当任务获得执行时,立即将 mPendingRebind 设为 false,以便后续其他 requestRebind 能往主线程发起 rebind 的任务。在 API 19 及以上的版本,检查下 UI 控件是否附加到了窗口上,如果没有附到窗口上,则设置监听器,以便在 UI 附加到窗口上的时候立即执行 rebind 操作,然后返回。当符合执行条件(API 19 以下或 UI 控件已经附加到窗口上)的时候,则调用 executePendingBindings 执行 binding 逻辑。

public void executePendingBindings() {
    if (mContainingBinding == null) {
        executeBindingsInternal();
    } else {
        mContainingBinding.executePendingBindings();
    }
}

private void executeBindingsInternal() {
    if (mIsExecutingPendingBindings) {
        requestRebind();
        return;
    }
    if (!hasPendingBindings()) {
        return;
    }
    mIsExecutingPendingBindings = true;
    mRebindHalted = false;
    if (mRebindCallbacks != null) {
        mRebindCallbacks.notifyCallbacks(this, REBIND, null);

        // The onRebindListeners will change mPendingHalted
        if (mRebindHalted) {
            mRebindCallbacks.notifyCallbacks(this, HALTED, null);
        }
    }
    if (!mRebindHalted) {
        executeBindings();
        if (mRebindCallbacks != null) {
            mRebindCallbacks.notifyCallbacks(this, REBOUND, null);
        }
    }
    mIsExecutingPendingBindings = false;
}

这里实际上还没有执行具体的 binding 操作,这里在执行前进行一些判定:

  1. 如果已经开始执行绑定操作了,即这段代码正在执行,那么调用一次 requestRebind,然后返回
  2. 如果当前没有需要进行刷新 UI 的需要,即脏标记为 0,那么直接返回。
  3. 接下来在执行具体的 executeBindings 操作前,调用下 mRebindCallbacks.notifyCallbacks,通知所有回调说即将开始 rebind 操作,回调可以在执行的过程中,将 mRebindHalted 置为 true,阻止 executeBindings 的运行,拦截成功同样通过回调进行通知。
  4. 如果没有被拦截, executeBindings 方法便得以运行,运行结束后,同样通过回调进行通知。

executeBindings 是个抽象方法,具体的实现在子类中。即跟 content_main.xml 相关的逻辑依旧内聚到了 ContentMainBinding 中。
executeBindings 的实现也是数据绑定框架在编译器生成的,如下:

@Override
protected void executeBindings() {
    long dirtyFlags = 0;
    synchronized(this) {
        dirtyFlags = mDirtyFlags;
        mDirtyFlags = 0;
    }
    // ... 省略变量声明

    if ((dirtyFlags & 0x9L) != 0) {
        if (presenter != null) {
            // read presenter::onClick
            presenterOnClickAndroidViewViewOnClickListener = (((mPresenterOnClickAndroidViewViewOnClickListener == null) 
                    ? (mPresenterOnClickAndroidViewViewOnClickListener = new OnClickListenerImpl()) 
                    : mPresenterOnClickAndroidViewViewOnClickListener).setValue(presenter));
        }
    }
    if ((dirtyFlags & 0xaL) != 0) {
    }
    if ((dirtyFlags & 0xcL) != 0) {
    }
    // batch finished
    if ((dirtyFlags & 0x9L) != 0) {
        // api target 1

        this.mboundView1.setOnClickListener(presenterOnClickAndroidViewViewOnClickListener);
    }
    if ((dirtyFlags & 0xcL) != 0) {
        // api target 1

        android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, firstName);
    }
    if ((dirtyFlags & 0x8L) != 0) {
        // api target 1

        android.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView2, (android.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (android.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (android.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView2androidTextAttrChanged);
        android.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView3, (android.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (android.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (android.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView3androidTextAttrChanged);
        this.mboundView4.setOnClickListener(mCallback1);
    }
    if ((dirtyFlags & 0xaL) != 0) {
        // api target 1

        android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView3, lastName);
    }
}

实现中,首先把脏标记位存到本地变量中,然后将脏标记位置为 0,开始批量处理之前的改动。根据脏标记位和相关的值进行位与运算来判断。
这里做了:

  1. 创建并设置回调,如 android:onClick="@{presenter::saveUserName}" 这种表达式,会在 presenter 不为空的情况下,创建对应的回调,并设置到相应的控件上;
  2. 将数据模型上的值更新到 UI 上,如将 firstName 设置到 mboundView1 上,lastName 设置到 mBoundView2 上。可以看到,每一个 标签声明的变量都有一个专属的标记位,当改变量的值被更新时,对应的脏标记位就会置为 1,executeBindings 的时候变回将这些变动更新到对应的控件。
  3. 在设置了双向绑定的控件上,为其添加对应的监听器,监听其变动,如:EditText 上设置了 TextWatcher。具体的设置逻辑放置到了 TextViewBindingAdapter.setTextWatcher 里。源码如下,也是创建一个新的 TextWatcher,将创建来的监听器包裹在其中。

方法数的问题

data binding 框架的 jar 包有两个,一个是 adapter,一个是 baseLibrary,前者方法数为 415,后者方法数为 502,整体增加的方法数不到一千个。生成的类方法数方面 demo 中大约是每个布局 20 个方法,具体跟布局内的变量数量(每个变量对应一个 get、set 方法)、双向绑定的数量(每个会多一个 InverseBindingListener 匿名类),会根据这几个因素有所浮动。


小结

将数据绑定在应用内的运行机制总结如下:

  1. 通过对 root view 进行一次遍历,将 view 中所有的控件查找出来并进行绑定,查找效率比使用 findViewById 更加高效
  2. 查找过程依赖于 view 的 tag 标记,尽量避免使用 tag 标记,以免跟干涉到框架的正常运行
  3. 对 UI 的操作都在主线程;对数据的操作可以在任意线程
  4. 对数据的操作并不会即时的反应到 UI 上,通过脏标记,往主线程发起 rebind 任务,在主线程下次回调的时候批量刷新,避免频繁操作 UI
  5. 使用数据绑定操作 UI 更加安全,操作集中在主线程,并在操作前进行空检查,避免空指针
  6. 绝大部分的逻辑在生成的 *Binding 类中,即数据绑定框架在编译期帮我们做了大量的工作,生成模板代码,实现绑定逻辑,是否为空检查,生成代理类,代码的可靠性也是由编译期的处理程序保证,有效的降低了人为出错的可能

你可能感兴趣的:([转] DataBinding 数据绑定)