前言
数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。
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类-->
<variable
name="User"
type="com.xixu.jetpack.User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android: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" />
<TextView
android: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
方法:
### DataBinderMapperImpl
public 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);
}
@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 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);
// listeners
invalidateAll();
}
其中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.onPropertyChanged
public void onPropertyChanged(Observable sender, int propertyId) {
//1.先判断绑定的ViewDataBinding是否为null
ViewDataBinding 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.username
userUsername = user.getUsername();
}
}
if ((dirtyFlags & 0xdL) != 0) {
if (user != null) {
// read User.pwd
userPwd = user.getPwd();
}
}
}
// batch finished
if ((dirtyFlags & 0xbL) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvName, userUsername);
}
if ((dirtyFlags & 0xdL) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvPwd, userPwd);
}
}
这里采用了取巧的方式处理,使用位运算操作计算要更新哪些控件,最终调用TextViewBindingAdapter.setText
完成数据更新;
更新数据流程图
总结
DataBinding
通过APT技术
于编译期生成相关辅助类,当调用DataBindingUtil.setContentView
方法时帮助我们完成布局控件绑定工作,减少FindViewById
操作,当调用数据更新操作时,数据作为被观察者,会绑定到WeakListener
虚引用对象上,更新过程中会使用Lifecycle进行生命周期判断,最终通过调用androidx.databinding.adapters
包下的辅助工具类完成控件更新操作;
结语
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )