目录
一、DataBinding应用场景
在Android开发初期,基本都会使用MVC模式,但随着业务的增加,Activity会变得很臃肿,业务代码与界面显示全部耦合在一起,扩展性差,维护起来很不方便,于是MVP模式就应运而生了。
MVP模式实现了V和M的解耦,分层清晰,把业务处理搬到P层,通过接口实现V层和P层之间通信。MVP模式也存在一些问题,随着项目的增大,接口类会越来越多,而且每一个小的改动,都需要通过接口来通信,有时候一个简单的功能,都需要写好几个接口,会造成接口爆炸。这时候MVVM模式就出现了。
MVVM模式同样也是解耦,将业务代码放到VM层,V层与VM层通过双向绑定来相互通知变化,不再需要那么多的接口来通信了。双向绑定,指的是将数据与界面绑定起来,当数据发生变化时会体现在界面上,反过来界面内容变化也会同步更新到数据上。
DataBinding可以轻松的实现MVVM,但并不能说DataBinding就只是属于MVVM的。DataBinding只是谷歌推出的一个支持双向绑定的库,它是工具集,能大大减少绑定app逻辑与layout文件的“胶水代码”,例如setText
、findViewById
等代码,大多数情况下,MVVM都使用DataBinding,但其实小部分情况下,MVP也会使用DataBinding。我们先来简单使用一下DataBinding,再对它的工作流程做一个大概的了解。
二、DataBinding使用
1、配置gradle来开启DataBinding
android {
...
buildFeatures {
dataBinding true
}
}
2、将布局转换为DataBinding布局
在xml布局中,点击小黄灯,选择Convert to data binding layout
此时会自动帮我们包裹一层<layout>
标签,这样就转换完成了
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".databinding.DataBindingActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
3、数据类
创建一个数据类来模拟网络请求返回的数据类,所有字段都定义为ObservableField
类型,并把实际类型通过泛型传入,这是kotlin的用法,和Java直接继承BaseObserver的写法稍有区别。
如果想要实现双向绑定,必须要使用ObservableField,这样才能收到数据变化的回调,从而实现M—>V一向的绑定,否则就只有V—>M单向的绑定了。
class User {
val name: ObservableField<String> by lazy { ObservableField() }
val age: ObservableField<String> by lazy { ObservableField() }
}
4、在xml的data标签中,引入数据类
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.zry.jetpackdemo.databinding.User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".databinding.DataBindingActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
5、双向绑定
当我们使用了DataBinding布局后,DataBinding会自动为我们生成一个以我们xml文件名字为依据创建的文件,比如我的xml文件名为activity_data_binding
,于是在Activity中我就可以直接使用DataBinding为我们生成的类ActivityDataBindingBinding
,如果没有的话,需要重新build一下,因为DataBinding采用的是APT技术,只有在编译期间才会生成代码。另外,还需要给DataBinding设置我们的数据类,这样DataBinding才可以监听数据的变化
class DataBindingActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityDataBindingBinding
private var mUser = User()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding)
mBinding.user = mUser
mUser.name.set("阿三")
mUser.age.set("30")
}
}
在xml中,定义几个TextView和Edittext来测试效果,我们使用两个TextView来分别显示name和age,两个Edittext分别可以输入来改变name和age
<?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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.zry.jetpackdemo.databinding.User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".databinding.DataBindingActivity">
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:text="@{user.name}"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_age"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@{user.age}"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name" />
<EditText
android:id="@+id/et_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:text="@={user.name}"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_age" />
<EditText
android:id="@+id/et_age"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:inputType="number"
android:text="@={user.age}"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
mBinding.user = mUser
mUser.name.set("阿三")
android:text="@{user.name}"
这些是实现了Model—>View层一向的绑定,数据类的值发生变化时,View上显示的值会马上改变
android:text="@={user.name}"
@={}实现了View—>Model层一向的绑定,当View上的值我们通过输入来改变以后,数据类的值会被相应修改
以上就实现了双向绑定。
三、DataBinding原理分析
DataBinding用起来很简单,越简单表示框架帮我们完成了越多的东西,在我们使用DataBinding时,编译后可以看到它帮我们生成了很多代码,毫无疑问自动生成的代码是通过APT技术来完成的。
1、拆布局
在布局转化为DataBinding布局时,DataBinding为根布局外再包裹了一层<layout>
标签,这个布局文件,在编译的期间,通过APT注解处理器,生成两个文件,一个是DataBinding内部使用的,记录了需要的布局信息的文件,一个是Android View体系本身的文件,也就是原本我们xml布局,不同的是,DataBinding把我们每个控件都加了一个tag
。我们来看一下这两个文件
第一个文件build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_data_binding-layout.xml
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="app\src\main\res\layout\activity_data_binding.xml"
isBindingData="true" isMerge="false" layout="activity_data_binding"
modulePackage="com.zry.jetpackdemo" rootNodeType="androidx.constraintlayout.widget.ConstraintLayout">
<Variables name="user" declared="true" type="com.zry.jetpackdemo.databinding.User">
<location endLine="9" endOffset="57" startLine="7" startOffset="8" />
</Variables>
<Targets>
<Target tag="layout/activity_data_binding_0"
view="androidx.constraintlayout.widget.ConstraintLayout">
<Expressions />
<location endLine="63" endOffset="55" startLine="13" startOffset="4" />
</Target>
<Target id="@+id/tv_name" tag="binding_1" view="TextView">
<Expressions>
<Expression attribute="android:text" text="user.name">
<Location endLine="23" endOffset="38" startLine="23" startOffset="12" />
<TwoWay>false</TwoWay>
<ValueLocation endLine="23" endOffset="36" startLine="23" startOffset="28" />
</Expression>
</Expressions>
<location endLine="27" endOffset="55" startLine="18" startOffset="8" />
</Target>
<Target id="@+id/tv_age" tag="binding_2" view="TextView">
<Expressions>
<Expression attribute="android:text" text="user.age">
<Location endLine="34" endOffset="37" startLine="34" startOffset="12" />
<TwoWay>false</TwoWay>
<ValueLocation endLine="34" endOffset="35" startLine="34" startOffset="28" />
</Expression>
</Expressions>
<location endLine="38" endOffset="63" startLine="29" startOffset="8" />
</Target>
<Target id="@+id/et_name" tag="binding_3" view="EditText">
<Expressions>
<Expression attribute="android:text" text="user.name">
<Location endLine="45" endOffset="39" startLine="45" startOffset="12" />
<TwoWay>true</TwoWay>
<ValueLocation endLine="45" endOffset="37" startLine="45" startOffset="29" />
</Expression>
</Expressions>
<location endLine="49" endOffset="62" startLine="40" startOffset="8" />
</Target>
<Target id="@+id/et_age" tag="binding_4" view="EditText">
<Expressions>
<Expression attribute="android:text" text="user.age">
<Location endLine="57" endOffset="38" startLine="57" startOffset="12" />
<TwoWay>true</TwoWay>
<ValueLocation endLine="57" endOffset="36" startLine="57" startOffset="29" />
</Expression>
</Expressions>
<location endLine="61" endOffset="63" startLine="51" startOffset="8" />
</Target>
</Targets>
</Layout>
可以看到每个Target节点都记录了布局中的控件的id以及tag,通过这些tag就可以读出布局,再来看一下拆过的第二个文件build/intermediates/incremental/mergeDebugResources/stripped.dir/layout/activity_data_binding.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".databinding.DataBindingActivity"
android:tag="layout/activity_data_binding_0"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:tag="binding_1"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_age"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:tag="binding_2"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name" />
<EditText
android:id="@+id/et_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:tag="binding_3"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_age" />
<EditText
android:id="@+id/et_age"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:inputType="number"
android:tag="binding_4"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_name" />
</androidx.constraintlayout.widget.ConstraintLayout>
这个文件其实就是我们自己编写的布局文件,但每个控件都被加上了tag。
2、DataBindingUtil.setContentView
DataBinding源码很庞大,但使用代码很少,我们来看一下最关键的这个方法,点进去可以看到其实也调用了activity.setContentView(layoutId);
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);
}
设置布局以后,拿到decorView
和contentView
,最后返回的是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);
}
}
可以看到bindToAddedViews
方法是在遍历所有子View,并最终调用bind
方法,跟进bind
方法再来看一下
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots,
int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId);
}
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}
这里的sMapper
是一个DataBinderMapper
对象,其实现类是DataBinderMapperImpl,DataBinderMapperImpl是通过APT注解处理器生成的。这里的sMapper.getDataBinder()
其实就是调用的MergedDataBinderMapper的getDataBinder()
方法,而sMapper
中的数据,其实就是DataBinderMapperImpl的构造器中调用其父类MergedDataBinderMapper的addMapper()
方法添加的对象
public class DataBinderMapperImpl extends MergedDataBinderMapper {
DataBinderMapperImpl() {
addMapper(new com.zry.jetpackdemo.DataBinderMapperImpl());
}
}
而MergedDataBinderMapper的getDataBinder()
方法,遍历了sMapper
,也就是拿到每个DataBinderMapperImpl去执行它的getDataBinder
方法
@Override
public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
int layoutId) {
for(DataBinderMapper mapper : mMappers) {
ViewDataBinding result = mapper.getDataBinder(bindingComponent, view, layoutId);
if (result != null) {
return result;
}
}
if (loadFeatures()) {
return getDataBinder(bindingComponent, view, layoutId);
}
return null;
}
再跟进一下getDataBinder
方法,DataBinderMapperImpl是APT注解处理器生成的类。如果是布局的顶层View,比如tag为layout/activity_data_binding_0
,那么就会new一个ActivityDataBindingBindingImpl对象。
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: {
//根布局tag就是这个
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;
}
ActivityDataBindingBindingImpl会进行一些View的绑定操作,将通过tag取出的View与
ActivityDataBindingBindingImpl中对应的View属性进行绑定。
public ActivityDataBindingBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
this(bindingComponent, root, mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds));
}
第三个参数为mapBindings
方法的返回结果,继续跟进看一下,进入了ViewDataBinding类中
protected static Object[] mapBindings(DataBindingComponent bindingComponent, View root,
int numBindings, IncludedLayouts includes, SparseIntArray viewsWithIds) {
Object[] bindings = new Object[numBindings];
mapBindings(bindingComponent, root, bindings, includes, viewsWithIds, true);
return bindings;
}
这里创建了一个Object数组,数组的大小为传进来的第三个参数,也就是布局的有tag的控件个数,继续跟进一下mapBindings
方法
private static void mapBindings(DataBindingComponent bindingComponent, View view,
Object[] bindings, IncludedLayouts includes, SparseIntArray viewsWithIds,
boolean isRoot) {
final int indexInIncludes;
// 判断View是否已经存在绑定,如果已经绑定,则直接return
final ViewDataBinding existingBinding = getBinding(view);
if (existingBinding != null) {
return;
}
// 获取View的tag标签
Object objTag = view.getTag();
final String tag = (objTag instanceof String) ? (String) objTag : null;
boolean isBound = false;
// 如果tag是根布局,并且是以layout开头的tag
if (isRoot && tag != null && tag.startsWith("layout")) {
final int underscoreIndex = tag.lastIndexOf('_');
if (underscoreIndex > 0 && isNumeric(tag, underscoreIndex + 1)) {
final int index = parseTagInt(tag, underscoreIndex + 1);
// 将根布局标签对应的View放在bindings数组中
if (bindings[index] == null) {
bindings[index] = view;
}
indexInIncludes = includes == null ? -1 : index;
isBound = true;
} else {
indexInIncludes = -1;
}
} else if (tag != null && tag.startsWith(BINDING_TAG_PREFIX)) {
int tagIndex = parseTagInt(tag, BINDING_NUMBER_START);
if (bindings[tagIndex] == null) {
bindings[tagIndex] = view;
}
isBound = true;
indexInIncludes = includes == null ? -1 : tagIndex;
} else {
// Not a bound view
indexInIncludes = -1;
}
if (!isBound) {
final int id = view.getId();
if (id > 0) {
int index;
if (viewsWithIds != null && (index = viewsWithIds.get(id, -1)) >= 0 &&
bindings[index] == null) {
bindings[index] = view;
}
}
}
if (view instanceof ViewGroup) {
final ViewGroup viewGroup = (ViewGroup) view;
final int count = viewGroup.getChildCount();
int minInclude = 0;
for (int i = 0; i < count; i++) {
final View child = viewGroup.getChildAt(i);
boolean isInclude = false;
if (indexInIncludes >= 0 && child.getTag() instanceof String) {
String childTag = (String) child.getTag();
if (childTag.endsWith("_0") &&
childTag.startsWith("layout") && childTag.indexOf('/') > 0) {
// This *could* be an include. Test against the expected includes.
int includeIndex = findIncludeIndex(childTag, minInclude,
includes, indexInIncludes);
if (includeIndex >= 0) {
isInclude = true;
minInclude = includeIndex + 1;
final int index = includes.indexes[indexInIncludes][includeIndex];
final int layoutId = includes.layoutIds[indexInIncludes][includeIndex];
int lastMatchingIndex = findLastMatching(viewGroup, i);
if (lastMatchingIndex == i) {
bindings[index] = DataBindingUtil.bind(bindingComponent, child,
layoutId);
} else {
final int includeCount = lastMatchingIndex - i + 1;
final View[] included = new View[includeCount];
for (int j = 0; j < includeCount; j++) {
included[j] = viewGroup.getChildAt(i + j);
}
bindings[index] = DataBindingUtil.bind(bindingComponent, included,
layoutId);
i += includeCount - 1;
}
}
}
}
if (!isInclude) {
mapBindings(bindingComponent, child, bindings, includes, viewsWithIds, false);
}
}
}
}
可以看出mapBindings
方法其实就是去寻找tag是layout_
开头的,tag是binding_
开头的,拿到这些设置了tag的view,最后都保存到Object[]中,这里其实是比较消耗内存的地方。至此,DataBinding就通过拆解布局,设置tag,将所有的view保存到了一个数组中。
3、设置数据
DataBindingUtil.setContentView
方法完成后,已经将所有布局交给DataBinding了。接下来再看一下DataBinding.setUser
方法,也就是设置数据类。在生成的ActivityDataBindingBindingImpl实现类中可以找到这个方法
public void setUser(@Nullable com.zry.jetpackdemo.databinding.User User) {
this.mUser = User;
synchronized(this) {
mDirtyFlags |= 0x4L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}
跟进一下requestRebind
方法
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; // wait until lifecycle owner is started
}
}
synchronized (this) {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
}
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
}
}
这个方法可以看到,DataBinding同样使用到了Lifecycle中的生命周期状态,最后会调用mChoreographer.postFrameCallback(mFrameCallback);
或mUIThreadHandler.post(mRebindRunnable);
,其实mFrameCallback
最后也会使用mRebindRunnable
,mRebindRunnable
在ViewDataBinding的构造中可以找到,run
方法内部执行代码如下:
synchronized(this) {
ViewDataBinding.this.mPendingRebind = false;
}
ViewDataBinding.processReferenceQueue();
if (VERSION.SDK_INT >= 19 && !ViewDataBinding.this.mRoot.isAttachedToWindow()) { ViewDataBinding.this.mRoot.removeOnAttachStateChangeListener(ViewDataBinding.ROOT_REATTACHED_LISTENER);
ViewDataBinding.this.mRoot.addOnAttachStateChangeListener(ViewDataBinding.ROOT_REATTACHED_LISTENER);
} else {
ViewDataBinding.this.executePendingBindings();
}
excutePendingBindings
方法最终会执行子类ActivityDataBindingBindingImpl中的executeBindings()
方法
@Override
protected void executeBindings() {
...
// batch finished
if ((dirtyFlags & 0xdL) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.etAge, userAgeGet);
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvAge, userAgeGet);
}
if ((dirtyFlags & 0x8L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.etAge, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, etAgeandroidTextAttrChanged);
androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.etName, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, etNameandroidTextAttrChanged);
}
if ((dirtyFlags & 0xeL) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.etName, userNameGet);
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvName, userNameGet);
}
}
...
}
到这里我们发现,最终执行的就是setText()
以及添加textWatcher
的操作,setText
就是M—>V一向,textWatcher
就是V—>M一向。至此DataBinding实现实现双向绑定的大致流程就分析完了,当然里面还有很多细节没有看,DataBinding代码量还是非常多的,在看源码时,要抓住主线流程,不然就会迷失在源码的海洋里。