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

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

什么是双向绑定

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

简单实例 

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

认识Android中的双向绑定_第1张图片

项目配置

在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"?>    

修改之后:

<?xml version="1.0" encoding="utf-8"?>                

其实很简单,就是把布局用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 content = new ObservableField<>();

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

<?xml version="1.0" encoding="utf-8"?>                                

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

@Overrideprotected void onResume() {    super.onResume();    content.set("这是设置的内容");}

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

在MainActivity中设置:

@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());    setContentView(binding.getRoot());    binding.setView(this);}

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

认识Android中的双向绑定_第2张图片

通知代码更改数据

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

<?xml version="1.0" encoding="utf-8"?>                                        

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

认识Android中的双向绑定_第3张图片

运行一把,还是不行。

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

认识Android中的双向绑定_第4张图片

原理

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

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

布局对比

编写布局:

<?xml version="1.0" encoding="utf-8"?>                                        

apk内布局:

<?xml version="1.0" encoding="utf-8"?>        

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

代码分析

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

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

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

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

@NonNullpublic 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又有什么操作呢?

@NonNullpublic 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():

@Overridepublic 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中

@Overrideprotected void executeBindings() {    long dirtyFlags = 0;    synchronized(this) {        dirtyFlags = mDirtyFlags;        mDirtyFlags = 0;    }    com.kevin.databindingtest.MainActivity view = mView;    android.databinding.ObservableField 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 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最基本的方法,只不过通过双向绑定的方式向开发者屏蔽了而已。

更多相关文章

  1. Android 之6.0 双向通话自动录音
  2. 浅谈Android五大布局
  3. Android中常用的五种布局方式:FrameLayout
  4. Android周学习Step By Step(4)--界面布局
  5. [androidUI]一些布局
  6. android远程绑定与本地绑定区别
  7. Android双向seekbar
  8. android 动态 布局

随机推荐

  1. Android旋转屏幕后国际化语言失效的解决
  2. android4.2 修改设置背景
  3. Android declare-styleable:自定义控件的
  4. Error:No resource identifier found for
  5. 获取手机安装的全部应用的示例教程
  6. :activity状态的保存和保持
  7. Android中两种设置全屏的方法
  8. 6.1、Android中从Internet获取数据
  9. Android providers 解析之telephony
  10. Android 系统常用权限