快看!DataBinding和LiveData搞在一起了

2022-05-10 20:39:13 浏览数 (1)

最近在工作中在一个页面使用了 DataBinding ,实践了一下 DatabindingLiveData 的结合。今天就来分享一下具体的内容。

代码语言:javascript复制
本篇文章适合对 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

第二步,把持有 LiveDataViewMoel 塞给 Databinding:

代码语言:javascript复制
<data>
    <variable
        name="vm"
        type="xxx.xxx.xxViewModel" />
</data>

第三步,绑定数据和 UI(这里假设 vm 里面有个 LiveData<String> 的 title):

代码语言:javascript复制
<TextView
    android:text=@{vm.title} />

第四步,在 ViewModel 里面改变 title 的值:

代码语言:javascript复制
title.postValue("fakeTitle")

到这里 “fakeTitle” 就被显示在了 TextView 上面。这时候疑惑出来了,这里 @{vm.title} 明明是一个 LiveData 啊,为什么能顺利的展示里面的 value 呢?那想必肯定是 Databinding 在绑定数据的时候把这个 value 取出来了,绑定的 LifecyclerOwner 也肯定是给这个 LiveData 使用的,不然肯定就不生效了。

既然能猜到大概,那干脆进源码直接看看具体是如何实现的吧。我们从 bind 的逻辑开始看起。

DataBindingUtilbind 方法最后会调用到

代码语言:javascript复制
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);
        }
    }
}

这里看到:布局文件和文件路径放在了 InnerLayoutIdLookupskeys 里,里面的变量都被放在了 InnerBrLookupsKeys 里,这俩 sKeys 都是一个 SparseArray,即一个 KV 结构里,

最后在 getDataBinder 的时候,会根据根 View 的tag,去实例化相应的 xxBindingImpl 对象。实例化时候使用的 View ,就是根据 sKeys 里面的 layout id 去 inflate 的。

在 BindingImpl 的构造方法中,会调用 super 的构造方法和 invalidateAll

直接看 invalidateAll,最终在调用父类 ViewDataBinding 的这个方法:

代码语言:javascript复制
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:

代码语言:javascript复制
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 变量:

代码语言:javascript复制
if (vm != null) {
    // read vm.port
    vmPort = vm.getPort();
}
updateLiveDataRegistration(0, vmPort);


if (vmPort != null) {
    // read vm.port.getValue()
    vmPortGetValue = vmPort.getValue();
}

查看 updateLiveDataRegistration :

代码语言:javascript复制
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_LISTENERCreateWeakListener

代码语言:javascript复制
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 的时候会构造出来然后传入:

代码语言:javascript复制
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 :

代码语言:javascript复制
private static class LiveDataListener implements Observer,
            ObservableReference<LiveData<?>>

实现了 androidx.lifecycle.Ovserverandroidx.databinding.ObservableReference,在构造函数里面:

代码语言:javascript复制
public LiveDataListener(ViewDataBinding binder, int localFieldId) {
    mListener = new WeakListener(binder, localFieldId, this);
}

创建了我们刚刚经常看到的 WeakListener, 继续看 setLifecycleOwner 方法:

代码语言:javascript复制
@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 绑定了 LifecyclerOwnerObserver, 查看数据变化的监听:

代码语言:javascript复制
binder.handleFieldChange(mListener.mLocalFieldId, mListener.getTarget(), 0);

这里就会进行 dirtyFlags 的处理和判断,如果需要刷新,那么会重新调用 ViewDatabindingrequestRebind 方法。然后把这个值的 value 塞给 TextView。当每次在 ViewModel 里面调用 LiveData 的 postValue 的时候,这个 Listener 都会执行,所以 UI 也可以更新了。

总结

到这里,我们就翻了一遍源码,弄清楚了 DataBinding 是怎么工作的,又是如何和 LiveData 相结合,绑定上生命周期的。

继续思考一下使用了 DataBinding 之后的代码写法:

  • View 只负责 UI 的处理和绑定
  • ViewModel 里面存储了数据,或者说是状态。在状态需要改变的时候调用 LiveData 的 postValue,UI 会自己发生改变
  • 更多的布局写在了 xml 文件

熟悉前端的同学可能会感觉似曾相识,这不就是:

HTML 里面写布局,jsx 中写了一堆 status,setState 更改状态的时候反映在 UI

思想和写法十分的相似。至于 DataBinding 的 @{} 表达式,那就和某些模板语言里面填的逻辑类比吧。GUI 编程在一定程度上,思想总是越来越相近和相似的。

0 人点赞