前言
数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。
DataBinding
支持双向绑定,数据变化的时候界面跟着变化,界面变化也同步给数据;
DataBinding
在MVVM模式中使用比较多,双向绑定机制实现了View和Model的同步更新。
简单使用
DataBinding
一般配合LiveData
和ViewModel
一起使用,这里就简单使用下,便于后续源码分析;
build.gradle
配置
buildFeatures {dataBinding true}
- 定义数据源
data class User(@Bindable var username: String = "", @Bindable var pwd: String = "") : BaseObservable()
- 定义数据源绑定的布局文件activity_data_binding.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"><data><!--data标签内定义数据源User类--><variablename="User"type="com.xixu.jetpack.User" /></data><androidx.constraintlayout.widget.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@+id/tvName"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@{User.username}"android:textSize="16sp"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintTop_toTopOf="parent" /><TextViewandroid:id="@+id/tvPwd"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:text="@{User.pwd}"android:textSize="16sp"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintTop_toBottomOf="@+id/tvName" /></androidx.constraintlayout.widget.ConstraintLayout>
</layout>
- 定义Activity使用
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)val dataBinding = DataBindingUtil.setContentView<ActivityDataBindingBinding>(this,R.layout.activity_data_binding)lifecycleScope.launch{delay(2000)dataBinding.user = User("XiXu", "123456")}}
}
界面效果:延迟2s后,分别将XiXu
、123456
数据绑定到tvName
、tvPwd
控件上;
源码分析
那DataBinding
是如何实现将数据绑定到具体视图上的呢?
DataBinding为我们生成了哪些布局文件
首先,DataBinding
会使用APT(Annotation Processing Too,注解解析器)
,在编译器为我们生成如下布局文件
由于使用DataBinding
,布局文件中引入了layout
标签,我们先看下布局文件变化;
1.build/intermediates/incremental/packageDebugResources/stripped.dir/layout/activity_data_binding.xml
,为每个控件都新增了tag
属性;
2.build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_data_binding-layout.xml
定义了多组Target
与布局文件中的tag
标签控件对应,并标注了每个tag对应控件的view类型,Expressions
标签中定义了控件属性
和对应绑定数据
,其中TwoWay
标签表示是否是双向绑定;
小结
使用DataBinding
会在编译期生成辅助布局文件,为每个控件新增tag
标签,并记录控件类型
、id
、属性
等信息;
DataBindingUtil.setContentView()做了什么
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,int layoutId) {return setContentView(activity, layoutId, sDefaultComponent);}public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,int layoutId, @Nullable DataBindingComponent bindingComponent) {activity.setContentView(layoutId);View decorView = activity.getWindow().getDecorView();ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);return bindToAddedViews(bindingComponent, contentView, 0, layoutId);}
调用setContentView
方法主要做了如下几件事:
- 调用activity对应的
setContentView
方法绑定布局; - 获取activity对应的
R.id.content
控件,我们知道即FrameLayout
控件; - 调用
bindToAddedViews
绑定布局中的控件;
这里我们重点看下bindToAddedViews
是如何实现布局控件绑定的;
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,ViewGroup parent, int startChildren, int layoutId) {final int endChildren = parent.getChildCount();final int childrenAdded = endChildren - startChildren;if (childrenAdded == 1) {final View childView = parent.getChildAt(endChildren - 1);return bind(component, childView, layoutId);} else {final View[] children = new View[childrenAdded];for (int i = 0; i < childrenAdded; i++) {children[i] = parent.getChildAt(i + startChildren);}return bind(component, children, layoutId);}}
这里parent
即指FrameLayout
,因此parent.getChildCount()
获取的便是根布局个数,上述例子对应的为1,即ConstraintLayout
;最终会调用bind(component, childView, layoutId)
方法如下:
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,int layoutId) {return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);}
其中sMapper
初始化代码如下:
private static DataBinderMapper sMapper = new DataBinderMapperImpl();public class DataBinderMapperImpl extends MergedDataBinderMapper {DataBinderMapperImpl() {addMapper(new com.crystal.maniu.DataBinderMapperImpl());}
}
因此sMapper.getDataBinder
最终调用的为DataBinderMapperImpl【注意包名,与androidx.databinding区分】.getDataBinder
方法:
### DataBinderMapperImplpublic class DataBinderMapperImpl extends DataBinderMapper {private static final int LAYOUT_ACTIVITYDATABINDING = 1;private static final SparseIntArray INTERNAL_LAYOUT_ID_LOOKUP = new SparseIntArray(1);static {INTERNAL_LAYOUT_ID_LOOKUP.put(com.crystal.maniu.R.layout.activity_data_binding, LAYOUT_ACTIVITYDATABINDING);}@Overridepublic 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 new RuntimeException("view must have a tag");}switch(localizedLayoutId) {case LAYOUT_ACTIVITYDATABINDING: {if ("layout/activity_data_binding_0".equals(tag)) {return new ActivityDataBindingBindingImpl(component, view);}throw new IllegalArgumentException("The tag for activity_data_binding is invalid. Received: " + tag);}}}return null;}}
可见getDataBinder
方法其实就是直接new了一个ActivityDataBindingBindingImpl【APT生成的类】对象,并把根布局ConstraintLayout入参
;我们看看ActivityDataBindingBindingImpl
构造方法里做了什么;
public ActivityDataBindingBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {this(bindingComponent, root, mapBindings(bindingComponent, root, 3, sIncludes, sViewsWithIds));}private ActivityDataBindingBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {super(bindingComponent, root, 1, (android.widget.TextView) bindings[1], (android.widget.TextView) bindings[2]);this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];this.mboundView0.setTag(null);this.tvName.setTag(null);this.tvPwd.setTag(null);setRootTag(root);// listenersinvalidateAll();}
其中mapBindings
方法会对根据tag对布局文件进行xml解析得到控件数组 Object[],super会调用父类ActivityDataBindingBinding【APT生成的类】的构造方法,完成View的初始化工作
, invalidateAll()
会进行数据的初始化绑定,具体如何绑定我们下一步在分析!
小结
调用DataBindingUtil.setContentView
方法会调用setContentView
方法并完成控件的初始化工作,从而代替FindViewById
的工作;
DataBinding如何实现数据更新
我们再看下调用dataBinding.user = User("XiXu", "123456")
是如何实现将数据更新到控件上去的,具体实现交给ActivityDataBindingBindingImpl.setUser
方法;
public void setUser(@Nullable com.xixu.jetpack.User User) {updateRegistration(0, User);this.mUser = User;synchronized(this) {mDirtyFlags |= 0x1L;}notifyPropertyChanged(BR.User);super.requestRebind();}
其中BR
文件如下,定义一系列常量用于区分更新字段:
public class BR {public static final int User = 1;public static final int _all = 0;public static final int pwd = 2;public static final int username = 3;
}
可以看到调用setUser
主要做了如下几件事:
-
调用updateRegistration(0, User)方法;
User
作为被观察者,会先判断User
是否为null,如果为null解除注册,否则会将User
绑定到WeakListener
虚引用对象上,并包装成WeakPropertyListener
对象,用于后续处理数据更新操作; -
调用
notifyPropertyChanged(BR.User)
方法;经过层层处理会执行WeakPropertyListener.onPropertyChanged
方法如下:
### WeakPropertyListener.onPropertyChangedpublic void onPropertyChanged(Observable sender, int propertyId) {//1.先判断绑定的ViewDataBinding是否为nullViewDataBinding binder = mListener.getBinder();if (binder == null) {return;}//2.obj即为传入的User对象,判断和之前绑定的是否一致Observable obj = mListener.getTarget();if (obj != sender) {return; }//3.调用ViewDataBinding.onFieldChange方法;binder.handleFieldChange(mListener.mLocalFieldId, sender, propertyId);}
ViewDataBinding.onFieldChange
代码如下:
protected void handleFieldChange(int mLocalFieldId, Object object, int fieldId) {if (mInLiveDataRegisterObserver || mInStateFlowRegisterObserver) {return;}boolean result = onFieldChange(mLocalFieldId, object, fieldId);if (result) {//内部会进行Lifecycle生命周期判断,从而实现更新逻辑与生命周期绑定requestRebind();}}protected void requestRebind() {if (mContainingBinding != null) {//设置包含绑定mContainingBinding.requestRebind();} else {//这里会进行组件活跃判断,如果不是STARTED、RESUMED状态,不执行更新数据操作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) {//SDK_INT >= 16 使用编舞者处理,最终还是调用mRebindRunnable.run方法mChoreographer.postFrameCallback(mFrameCallback);} else {mUIThreadHandler.post(mRebindRunnable);}}}
其中onFieldChange
调用的是ActivityDataBindingBindingImpl.onFieldChange
方法根据mLocalFieldId
以及fieldId
匹配BR文件中的常量,判断能否更新;
如果返回true,则调用requestRebind()
方法,最终又会回到 ActivityDataBindingBindingImpl.executeBindings方法
;
protected void executeBindings() {long dirtyFlags = 0;synchronized(this) {dirtyFlags = mDirtyFlags;mDirtyFlags = 0;}com.xixu.jetpack.User user = mUser;java.lang.String userUsername = null;java.lang.String userPwd = null;if ((dirtyFlags & 0xfL) != 0) {if ((dirtyFlags & 0xbL) != 0) {if (user != null) {// read User.usernameuserUsername = user.getUsername();}}if ((dirtyFlags & 0xdL) != 0) {if (user != null) {// read User.pwduserPwd = user.getPwd();}}}// batch finishedif ((dirtyFlags & 0xbL) != 0) {// api target 1androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvName, userUsername);}if ((dirtyFlags & 0xdL) != 0) {// api target 1androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvPwd, userPwd);}}
这里采用了取巧的方式处理,使用位运算操作计算要更新哪些控件,最终调用TextViewBindingAdapter.setText
完成数据更新;
更新数据流程图
总结
DataBinding
通过APT技术
于编译期生成相关辅助类,当调用DataBindingUtil.setContentView
方法时帮助我们完成布局控件绑定工作,减少FindViewById
操作,当调用数据更新操作时,数据作为被观察者,会绑定到WeakListener
虚引用对象上,更新过程中会使用Lifecycle进行生命周期判断,最终通过调用androidx.databinding.adapters
包下的辅助工具类完成控件更新操作;
结语
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )