认识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中的双向绑定的主要内容,如果未能解决你的问题,请参考以下文章