认识Android中的双向绑定

Posted 周文凯

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了认识Android中的双向绑定相关的知识,希望对你有一定的参考价值。

转载请标明出处:
http://blog.csdn.net/xuehuayous/article/details/81100571
本文出自:【Kevin.zhou的博客】

前言:在和一些朋友&网友聊的过程中,发现很多人对于android中的双向绑定还不太了解,所以MVVM架构就比较难以向大家描述清楚,那么先来了解一下Android中的双向绑定。

什么是双向绑定

双向绑定到底是什么,查了很久没有找到比较好的解释,这里说下我的理解,通过监听机制保持多处数据同步的思想。

简单实例 

为了便于理解,我们编写一个简单的示例。一个TextView,一个EditText,EditText输入内容TextView随着显示。

项目配置

在app Module的build.gradle中添加dataBinding的支持。

android 
    // ...

    dataBinding 
        enabled true
    

在Android Studio 1.3及以上和Android Plugin for Gradle: 1.5.0-alpha1及以上环境,Android Studio就会读取配置,自动加入如下依赖,我们不用手动加入。可以了解到databinding是通过编译期生成代码的方式实现的。

dependencies 
    implementation 'com.android.databinding:library:3.1.3'
    implementation 'com.android.databinding:adapters:3.1.3'
    annotationProcessor 'com.android.databinding:compiler:3.1.3'

DataBinding方式设置布局

修改布局

修改之前:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

</LinearLayout>

修改之后:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!" />

    </LinearLayout>
</layout>

其实很简单,就是把布局用layout标签包裹起来了,并且把schemas约束移到了layout标签下,其实不动也行,感觉移出去好看点。

修改代码

修改之前:

public class MainActivity extends AppCompatActivity 

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    

修改之后:

public class MainActivity extends AppCompatActivity 

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
    

添加了一行代码,获取ActivityMainBinding(ActivityMainBinding是根据我们XML布局的名称生产的,布局activity_main.xml对应ActivityMainBinding,当然也可以在XML布局的data标签的class属性进行指定,如果没有自动生成build一下项目肯定有了),并且setContentView的时候设置binding的getRoot()。

使用DataBinding方式设置布局,看着修改比较多,其实就只动了两行代码而已。

通知布局更改数据

在MainActivity中添加可观察对象,用来保存数据内容:

public ObservableField<String> content = new ObservableField<>();

在布局中添加view变量,在TextView使用 android:text="@view.content" 来监听MainActivity中可观察对象的数据变化:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="view"
            type="com.kevin.databindingtest.MainActivity" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@view.content" />

    </LinearLayout>
</layout>

然后,在MainAcitvity的onResume中动态设置可观察对象content的值:

@Override
protected void onResume() 
    super.onResume();
    content.set("这是设置的内容");

兴奋的我们,运行了下,可是什么也没有显示。是由于没有给布局中的view变量设置值,TextView自然不知道要监听谁的content字段。

在MainActivity中设置:

@Override
protected void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
    ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
    setContentView(binding.getRoot());
    binding.setView(this);

这样就实现了,布局监听代码中数据,数据更改时动态显示在居中的控件上。

通知代码更改数据

通过上面的简单实例就实现了布局监听数据的变化,这叫单向绑定,和微信小程序有点相似。那说好的双向绑定呢?我们把布局中的添加一个EditText,这样布局的数据就可以更改了。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="view"
            type="com.kevin.databindingtest.MainActivity" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@view.content" />

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@view.content" />

    </LinearLayout>
</layout>

这样就可以实现布局的数据更改啦,我们预计应该是EditText更改内容,然后显示在TextView上。

运行一把,还是不行。

只要把EditText上加上一个等号就可以啦,这里的等号表示content会监听EditText内容的变化。这种语法是databinding的约定,在编译期databinding会根据这种标识生成具体的监听代码,具体的后面再讲。

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@=view.content" />

原理

见识了这么牛掰的东西,我们不禁连连称奇,要知道之前写一个这样的功能是非常繁琐的,而且熟悉的findViewById,setOnXxxListener都不见了。是不是已经迫不及待的想要知道其中的原理呢?

其实原理也比较简单,我们先对比下编写的布局和最终apk内的布局对比。

布局对比

编写布局:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="view"
            type="com.kevin.databindingtest.MainActivity" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@view.content" />

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@=view.content" />

    </LinearLayout>
</layout>

apk内布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="1"
    android:tag="layout/activity_main_0"
    android:layout_width="-1"
    android:layout_height="-1">

    <TextView
        android:tag="binding_1"
        android:layout_width="-2"
        android:layout_height="-2" />

    <EditText
        android:tag="binding_2"
        android:layout_width="-1"
        android:layout_height="-2" />
</LinearLayout>

对比发现,我们加载外面的layout标签没有了,设置的数据绑定也没有了,取而代之的是每个控件都多了个tag,最外层的LinearLayout的tag为android:tag="layout/activity_main_0",那也就是说在apk打包的时候对布局进行了修改,而且我们在代码中使用了ActivityMainBinding,这个东西明显不是我们编写的,是在编译时生成的。那就是顺着在MainActivity中的调用,看下具体做了哪些事情。

代码分析

@Override
protected void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
    // 调用 ActivityMainBinding 的 inflate,返回 ActivityMainBinding 对象
    ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
    setContentView(binding.getRoot());
    binding.setView(this);

看样子所有操作的入口就是在ActivityMainBinding的inflate()方法,那么就从这里开始分析吧。

@NonNull
public static ActivityMainBinding inflate(@NonNull android.view.LayoutInflater inflater) 
    // 调用两个参数的方法
    return inflate(inflater, android.databinding.DataBindingUtil.getDefaultComponent());

我们调用的一个参数的inflate()方法调用了两个参数的inflate()方法。

@NonNull
public static ActivityMainBinding inflate(@NonNull android.view.LayoutInflater inflater, @Nullable android.databinding.DataBindingComponent bindingComponent) 
    return bind(inflater.inflate(com.kevin.databindingtest.R.layout.activity_main, null, false), bindingComponent);

这里通过inflater将R.layout.activity_main布局填充为View,然后作为参数传递给bind方法,bing又有什么操作呢?

@NonNull
public static ActivityMainBinding bind(@NonNull android.view.View view, @Nullable android.databinding.DataBindingComponent bindingComponent) 
    // 由于编译后的布局添加了tag,这里是肯定成立的
    if (!"layout/activity_main_0".equals(view.getTag())) 
        throw new RuntimeException("view tag isn't correct on view:" + view.getTag());
    
    return new ActivityMainBinding(bindingComponent, view);

这里返回了ActivityMainBinding对象,那ActivityMainBinding的构造参数中应该做了很多事情,来看一下:

public ActivityMainBinding(@NonNull android.databinding.DataBindingComponent bindingComponent, @NonNull View root) 
    super(bindingComponent, root, 1);
    // 通过Tag查找布局中所有View并添加到数组中
    final Object[] bindings = mapBindings(bindingComponent, root, 3, sIncludes, sViewsWithIds);
    // 给View赋值,然后清除Tag
    this.mboundView0 = (android.widget.LinearLayout) bindings[0];
    this.mboundView0.setTag(null);
    this.mboundView1 = (android.widget.TextView) bindings[1];
    this.mboundView1.setTag(null);
    this.mboundView2 = (android.widget.EditText) bindings[2];
    this.mboundView2.setTag(null);
    setRootTag(root);
    // listeners
    invalidateAll();

首先调用了父类的构造参数,然后根据编译后给布局设置的Tag值来获取View,并赋值给ActivityMainBinding的变量,之后清除编译时设置的Tag,最后添加监听。父类(ViewDataBinding)的构造函数主要做了什么呢?

protected ViewDataBinding(DataBindingComponent bindingComponent, View root, int localFieldCount) 
    mBindingComponent = bindingComponent;
    mLocalFieldObservers = new WeakListener[localFieldCount];
    this.mRoot = root;
    // 校验是否主线程
    if (Looper.myLooper() == null) 
        throw new IllegalStateException("DataBinding must be created in view's UI Thread");
    
    // 初始化mChoreographer,后面会用到。系统版本 >= 16,版本兼容用
    if (USE_CHOREOGRAPHER) 
        mChoreographer = Choreographer.getInstance();
        mFrameCallback = new Choreographer.FrameCallback() 
            @Override
            public void doFrame(long frameTimeNanos) 
                mRebindRunnable.run();
            
        ;
     else 
        mFrameCallback = null;
        mUIThreadHandler = new Handler(Looper.myLooper());
    

通过以上的分析,我们大致知道了设置布局,初始化控件,那么核心的双向绑定在哪呢?大兄弟,别着急,马上就到了。回到ActivityMainBinding的构造方法,该方法最后调用了invalidateAll():

@Override
public void invalidateAll() 
    synchronized(this) 
            mDirtyFlags = 0x4L;
    
    requestRebind();

将mDirtyFlags设置为了0x4L,然后调用了requestRebind()。

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
            
        
        // 系统版本 >= 16,版本兼容
        if (USE_CHOREOGRAPHER) 
            mChoreographer.postFrameCallback(mFrameCallback);
         else 
            mUIThreadHandler.post(mRebindRunnable);
        
    

略过校验和目前不关心代码,直接看mChoreographer.postFrameCallback(mFrameCallback);还记得在上面ActivityMainBinding调用父类构造方法时创建了mChoreographer对象。

mChoreographer = Choreographer.getInstance();
mFrameCallback = new Choreographer.FrameCallback() 
    @Override
    public void doFrame(long frameTimeNanos) 
        mRebindRunnable.run();
    
;

关于postFrameCallback简单理解就是在渲染下一帧的时候渲染指定的内容,那这里指定的mRebindRunnable是什么呢?

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();
    
;

略过不重要的,直接看最下面的executePendingBindings();

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

由于没有初始化mContainingBinding,这里会执行executeBindingsInternal();方法:

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;

经过一系列的检测,执行到executeBindings(),通过名字就能看到浓浓的绑定气息。

protected abstract void executeBindings();

在ViewDataBinding中该方法是抽象的,这样又回到了我们的ActivityMainBinding中

@Override
protected void executeBindings() 
    long dirtyFlags = 0;
    synchronized(this) 
        dirtyFlags = mDirtyFlags;
        mDirtyFlags = 0;
    
    com.kevin.databindingtest.MainActivity view = mView;
    android.databinding.ObservableField<java.lang.String> viewContent = null;
    java.lang.String viewContentGet = null;

    if ((dirtyFlags & 0x7L) != 0) 
            if (view != null) 
                // read view.content
                viewContent = view.content;
            
            updateRegistration(0, viewContent);
            if (viewContent != null) 
                // read view.content.get()
                viewContentGet = viewContent.get();
            
    
    // batch finished
    if ((dirtyFlags & 0x7L) != 0) 
        // api target 1
        android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, viewContentGet);
        android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, viewContentGet);
    
    if ((dirtyFlags & 0x4L) != 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);
    

还记得我们在前面设置的invalidateAll()方法中把mDirtyFlags 设置为了 0x4L不?

由于4&7=4不等与0,则执行 read view.content的操作,然后给布局中的两个View(TextView、EditText)设置值。

由于4&4=4不等于0,则给EditText添加输入监听。

这里有两个比较核心的点,调用TextViewBindingAdapter#setText(view, text)给指定View设置值,调用TextViewBindingAdapter#setTextWatcher(view, beforeTextChange, onTextChange, afterTextChange),给EditText设置监听。

@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) 
    final CharSequence oldText = view.getText();
    if (text == oldText || (text == null && oldText.length() == 0)) 
        return;
    
    if (text instanceof Spanned) 
        if (text.equals(oldText)) 
            return; // No change in the spans, so don't set anything.
        
     else if (!haveContentsChanged(text, oldText)) 
        return; // No content changes, so don't set anything.
    
    view.setText(text);

setText(view, text)方法比较简单,当TextView内容变化的时候进行赋值,由于EditText是TextView的子类,也是使用的这种方式赋值。细心的朋友发现了,该方法的头上有个 @BindingAdapter("android:text") 注解,它的作用是什么呢?还记得我们之前设置值时使用的 android:text="@view.content",databinding通过这种方式对TextView的属性进行了扩展,当然我们也可以通过属性扩展自己想要的,比如给ImageView扩展一个imageUrl属性,直接给它一个url地址就可以显示图片。如果大家想了解原理,可以在后面评论下,我再给大家细讲扩展属性的原理。

还有一个EditText设置监听的方法:

@BindingAdapter(value = "android:beforeTextChanged", "android:onTextChanged",
        "android:afterTextChanged", "android:textAttrChanged", requireAll = false)
public static void setTextWatcher(TextView view, final BeforeTextChanged before,
        final OnTextChanged on, final AfterTextChanged after,
        final InverseBindingListener textAttrChanged) 
    final TextWatcher newValue;
    if (before == null && after == null && on == null && textAttrChanged == null) 
        newValue = null;
     else 
        newValue = new TextWatcher() 
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) 
                if (before != null) 
                    before.beforeTextChanged(s, start, count, after);
                
            

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) 
                if (on != null) 
                    on.onTextChanged(s, start, before, count);
                
                if (textAttrChanged != null) 
                    textAttrChanged.onChange();
                
            

            @Override
            public void afterTextChanged(Editable s) 
                if (after != null) 
                    after.afterTextChanged(s);
                
            
        ;
    
    final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
    if (oldValue != null) 
        view.removeTextChangedListener(oldValue);
    
    if (newValue != null) 
        view.addTextChangedListener(newValue);
    

由于我们设置监听时如下,只传递了最后一个AfterTextChange的监听参数。

TextViewBindingAdapter.setTextWatcher(this.mboundView2, null, null, null, mboundView2androidTextAttrChanged);

那么这里的mboundView2androidTextAttrChanged内容是什么呢?大家可以先猜想一下。

private android.databinding.InverseBindingListener mboundView2androidTextAttrChanged = new android.databinding.InverseBindingListener() 
    @Override
    public void onChange() 
        // Inverse of view.content.get()
        //         is view.content.set((java.lang.String) callbackArg_0)
        java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView2);
        // localize variables for thread safety
        // view.content
        android.databinding.ObservableField<java.lang.String> viewContent = null;
        // view.content.get()
        java.lang.String viewContentGet = null;
        // view.content != null
        boolean viewContentJavaLangObjectNull = false;
        // view
        com.kevin.databindingtest.MainActivity view = mView;
        // view != null
        boolean viewJavaLangObjectNull = false;

        viewJavaLangObjectNull = (view) != (null);
        if (viewJavaLangObjectNull) 
            viewContent = view.content;
            viewContentJavaLangObjectNull = (viewContent) != (null);
            if (viewContentJavaLangObjectNull) 

                viewContent.set(((java.lang.String) (callbackArg_0)));
            
        
    
;

没错,和我们猜想的一样,就是获取EditText的值然后设置到TextView,不过这里做了很多的安全校验,那我们就可以放心的开车撸码了。

总结

通过本篇,已经对Android中的双向绑定有了初步认识,双向绑定可以分为两个方向,一是XML中控件监听Activity/Fragment等View的数据变化并显示出来,二是View监听XML布局的数据变化并记录下来。

对其源码进行了分析,了解了其实现原理。只不过是通过设置一些标记,生成中间产物,其实还是Android最基本的方法,只不过通过双向绑定的方式向开发者屏蔽了而已。

以上是关于认识Android中的双向绑定的主要内容,如果未能解决你的问题,请参考以下文章

双向数据绑定和单项数据绑定的认识

Android,DataBinding的官方双向绑定

Angularjs中不同类型的双向数据绑定

浅谈Android开发中的MVVM模式

Vue双向绑定原理,教你一步一步实现双向绑定

谈谈对vue的认识: