最近在工作中在一个页面使用了 DataBinding
,实践了一下 Databinding
和 LiveData
的结合。今天就来分享一下具体的内容。
本篇文章适合对 Databinding 及 LiveData 有过了解(阅读过文档即可),阅读时间约 20 分钟
在过完 Databinding
文档的时候,首先看到了利用 Observable
接口的子类来实现使用可观察数据对象。但是 Observable
对象毕竟是不可以与生命周期绑定的,数据监听我们还是比较希望能使用 LiveData
去做。当然后面官方也提供了和 LiveData
结合的使用方式。可以将数据变化自动通知给 UI。
大概步骤其实很简单,第一步指定生命周期的 owner:
代码语言:javascript复制val view = getLayoutInflater().inflate(R.layout.xx, null, false);
binding = DataBindingUtil.bind(view)
setContentView(view)
binding?.lifecycleOwner = this
第二步,把持有 LiveData
的 ViewMoel
塞给 Databinding
:
<data>
<variable
name="vm"
type="xxx.xxx.xxViewModel" />
</data>
第三步,绑定数据和 UI(这里假设 vm 里面有个 LiveData<String>
的 title):
<TextView
android:text=@{vm.title} />
第四步,在 ViewModel
里面改变 title 的值:
title.postValue("fakeTitle")
到这里 “fakeTitle” 就被显示在了 TextView 上面。这时候疑惑出来了,这里 @{vm.title}
明明是一个 LiveData
啊,为什么能顺利的展示里面的 value 呢?那想必肯定是 Databinding
在绑定数据的时候把这个 value 取出来了,绑定的 LifecyclerOwner
也肯定是给这个 LiveData
使用的,不然肯定就不生效了。
既然能猜到大概,那干脆进源码直接看看具体是如何实现的吧。我们从 bind
的逻辑开始看起。
DataBindingUtil
的 bind
方法最后会调用到
public static <T extends ViewDataBinding> T bind(@NonNull View root,
DataBindingComponent bindingComponent)
这个方法:
代码语言:javascript复制T binding = getBinding(root);
if (binding != null) {
return binding;
}
Object tagObj = root.getTag();
if (!(tagObj instanceof String)) {
// throw
} else {
String tag = (String) tagObj;
int layoutId = sMapper.getLayoutId(tag);
if (layoutId == 0) {
// throw
}
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}
这里能看到,每个 binding 如果绑定过了是会缓存下来直接使用的。新绑定的时候会从 sMapper 这个对象中去取。
点进去看这个 sMapper
,是一个 DataBinderMapper
对象的子类,看一下它的子类:
每个 module 都会生成他的子类,那么进相对应的包看,这里抽取出关键的逻辑(我把其中一个 Activity 的名字改为 test):
代码语言:javascript复制public class DataBinderMapperImpl extends DataBinderMapper {
private static final int LAYOUT_ACTIVITYTEST = 1;
private static final SparseIntArray INTERNAL_LAYOUT_ID_LOOKUP = new SparseIntArray(9);
static {
INTERNAL_LAYOUT_ID_LOOKUP.put(com.xx.R.layout.activity_test, LAYOUT_ACTIVITYTEST);
}
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
if(localizedLayoutId > 0) {
final Object tag = view.getTag();
if(tag == null) {
// throw
}
switch(localizedLayoutId) {
case LAYOUT_ACTIVITYTEST: {
if ("layout/activity_test_0".equals(tag)) {
return new ActivityTestBindingImpl(component, view);
}
// throw
}
}
}
}
@Override
public int getLayoutId(String tag) {
if (tag == null) {
return 0;
}
Integer tmpVal = InnerLayoutIdLookup.sKeys.get(tag);
return tmpVal == null ? 0 : tmpVal;
}
private static class InnerBrLookup {
static final SparseArray<String> sKeys = new SparseArray<String>(51);
static {
sKeys.put(48, "vm");
}
}
private static class InnerLayoutIdLookup {
static final HashMap<String, Integer> sKeys = new HashMap<String, Integer>(9);
static {
sKeys.put("layout/activity_test_0", com.xx.R.layout.activity_test);
}
}
}
这里看到:布局文件和文件路径放在了 InnerLayoutIdLookup
的 skeys
里,里面的变量都被放在了 InnerBrLookup
的 sKeys
里,这俩 sKeys
都是一个 SparseArray
,即一个 KV 结构里,
最后在 getDataBinder
的时候,会根据根 View 的tag,去实例化相应的 xxBindingImpl
对象。实例化时候使用的 View
,就是根据 sKeys
里面的 layout id 去 inflate 的。
在 BindingImpl 的构造方法中,会调用 super 的构造方法和 invalidateAll
直接看 invalidateAll
,最终在调用父类 ViewDataBinding
的这个方法:
protected void requestRebind() {
if (mContainingBinding != null) {
mContainingBinding.requestRebind();
} else {
final LifecycleOwner owner = this.mLifecycleOwner;
if (owner != null) {
Lifecycle.State state = owner.getLifecycle().getCurrentState();
if (!state.isAtLeast(Lifecycle.State.STARTED)) {
return;
}
}
synchronized (this) {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
}
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
}
}
重点就是下面那个 if-else 的逻辑了:
代码语言:javascript复制USE_CHOREOGRAPHER = SDK_INT >= 16;
看下 USE_CHOREOGRAPHER 的定义,那么我们基本上可以直接关注第一个分支:
这里直接开始下一个 frame 的运行,找到这个 callback 的定义:
代码语言:javascript复制mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
mRebindRunnable.run();
}
};
还是调用了 mRebindRunnable:
代码语言:javascript复制synchronized (this) {
mPendingRebind = false;
}
processReferenceQueue();
if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
if (!mRoot.isAttachedToWindow()) {
mRoot.removeOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
return;
}
}
executePendingBindings();
processReferenceQueue 中解除了所有的 WeakListener
, 至于这个是干嘛的,我们稍后再关注。
先看 executePendingBindings
:
public void executePendingBindings() {
if (mContainingBinding == null) {
executeBindingsInternal();
} else {
mContainingBinding.executePendingBindings();
}
}
看看这个 mContainingBinding,注释是这样的:
代码语言:javascript复制/**
* If this ViewDataBinding is an include in another ViewDataBinding, this is the one
* that contains this. mContainingBinding is used to order executeBindings -- containing
* bindings should execute prior to included bindings.
*/
可以看到这个变量是用来处理 databinding 的布局里面嵌套 databinding 的复杂情况的,我们暂时只关注非嵌套的逻辑,最终会调用到 executeBindings
方法,这是一个 abstract 方法,那么具体的实现应该就在 DataBinding
给我们实现的 Java 代码里了,这里给出一个实际的代码生成的例子:
这里可以看到这个方法里面有各种各样的变量,都和我们写的绑定文件里面声明的变量相关。我们分几个点来关注,
第一个进入视线的就是 dirtyFlags
这个变量,熟悉 Flutter 的同学应该知道,布局树在 rebuild 的时候会有一个 dirty 标记来防止 rebuild 不需要 rebuild 的 Widget,那么这里的 dirtyFlags 是不是也是类似的作用呢?
看看哪里给 mDirtyFlags
赋值了:
随便看一个:
在设置 wsstatus 这个变量的时候,mDirtyFlags 和 0x20L 做了一次或运算。
最后在设置这个值的时候会做一次与运算来判断这个脏标记是否为0,如果不为0,那么这个数据就需要更新到UI上。避免了一个数据变化更新整个布局文件上的 View 的状态。
再来看一下 vm 里面的 LiveData
是怎么反映到 UI 上的。我的 ViewModel
里面有一个 LiveData<String>
的 port 变量:
if (vm != null) {
// read vm.port
vmPort = vm.getPort();
}
updateLiveDataRegistration(0, vmPort);
if (vmPort != null) {
// read vm.port.getValue()
vmPortGetValue = vmPort.getValue();
}
查看 updateLiveDataRegistration
:
private boolean updateRegistration(int localFieldId, Object observable,
CreateWeakListener listenerCreator) {
if (observable == null) {
return unregisterFrom(localFieldId);
}
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
registerTo(localFieldId, observable, listenerCreator);
return true
}
if (listener.getTarget() == observable) {
return false;//nothing to do, same object
}
unregisterFrom(localFieldId);
registerTo(localFieldId, observable, listenerCreator);
return true;
}
这里传入了一个 CREATE_LIVE_DATA_LISTENER
的 CreateWeakListener
:
private static final CreateWeakListener CREATE_LIVE_DATA_LISTENER = new CreateWeakListener() {
@Override
public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
return new LiveDataListener(viewDataBinding, localFieldId).getListener();
}
};
返回了一个 LiveDataListener
对象,这里构造的对象在没有绑定 WeakListener 的时候会构造出来然后传入:
protected void registerTo(int localFieldId, Object observable,
CreateWeakListener listenerCreator) {
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
listener = listenerCreator.create(this, localFieldId);
mLocalFieldObservers[localFieldId] = listener;
if (mLifecycleOwner != null) {
listener.setLifecycleOwner(mLifecycleOwner);
}
}
listener.setTarget(observable);
}
那我们看看 LiveDataListener
:
private static class LiveDataListener implements Observer,
ObservableReference<LiveData<?>>
实现了 androidx.lifecycle.Ovserver
和 androidx.databinding.ObservableReference
,在构造函数里面:
public LiveDataListener(ViewDataBinding binder, int localFieldId) {
mListener = new WeakListener(binder, localFieldId, this);
}
创建了我们刚刚经常看到的 WeakListener
, 继续看 setLifecycleOwner
方法:
@Override
public void setLifecycleOwner(LifecycleOwner lifecycleOwner) {
LifecycleOwner owner = (LifecycleOwner) lifecycleOwner;
LiveData<?> liveData = mListener.getTarget();
if (liveData != null) {
if (mLifecycleOwner != null) {
liveData.removeObserver(this);
}
if (lifecycleOwner != null) {
liveData.observe(owner, this);
}
}
mLifecycleOwner = owner;
}
这里可以看到, LiveData
绑定了 LifecyclerOwner
和 Observer
, 查看数据变化的监听:
binder.handleFieldChange(mListener.mLocalFieldId, mListener.getTarget(), 0);
这里就会进行 dirtyFlags 的处理和判断,如果需要刷新,那么会重新调用 ViewDatabinding
的 requestRebind
方法。然后把这个值的 value 塞给 TextView。当每次在 ViewModel 里面调用 LiveData 的 postValue 的时候,这个 Listener 都会执行,所以 UI 也可以更新了。
总结
到这里,我们就翻了一遍源码,弄清楚了 DataBinding
是怎么工作的,又是如何和 LiveData
相结合,绑定上生命周期的。
继续思考一下使用了 DataBinding
之后的代码写法:
- View 只负责 UI 的处理和绑定
- ViewModel 里面存储了数据,或者说是状态。在状态需要改变的时候调用
LiveData
的 postValue,UI 会自己发生改变 - 更多的布局写在了 xml 文件
熟悉前端的同学可能会感觉似曾相识,这不就是:
HTML 里面写布局,jsx 中写了一堆 status,setState 更改状态的时候反映在 UI
思想和写法十分的相似。至于 DataBinding
的 @{} 表达式,那就和某些模板语言里面填的逻辑类比吧。GUI 编程在一定程度上,思想总是越来越相近和相似的。