认识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"?>
修改之后:
<?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);}
这样就实现了,布局监听代码中数据,数据更改时动态显示在居中的控件上。
通知代码更改数据
通过上面的简单实例就实现了布局监听数据的变化,这叫单向绑定,和微信小程序有点相似。那说好的双向绑定呢?我们把布局中的添加一个EditText,这样布局的数据就可以更改了。
<?xml version="1.0" encoding="utf-8"?>
这样就可以实现布局的数据更改啦,我们预计应该是EditText更改内容,然后显示在TextView上。
运行一把,还是不行。
只要把EditText上加上一个等号就可以啦,这里的等号表示content会监听EditText内容的变化。这种语法是databinding的约定,在编译期databinding会根据这种标识生成具体的监听代码,具体的后面再讲。
原理
见识了这么牛掰的东西,我们不禁连连称奇,要知道之前写一个这样的功能是非常繁琐的,而且熟悉的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最基本的方法,只不过通过双向绑定的方式向开发者屏蔽了而已。
更多相关文章
- Android 之6.0 双向通话自动录音
- 浅谈Android五大布局
- Android中常用的五种布局方式:FrameLayout
- Android周学习Step By Step(4)--界面布局
- [androidUI]一些布局
- android远程绑定与本地绑定区别
- Android双向seekbar
- android 动态 布局