本文介绍了Data Binding的原理。
关于Data Binding的使用请查看Data Binding。
原文链接:Understanding Data-Binding’s generated code and How does Android Data-Binding compiler work
这篇文章并不是介绍怎样使用Data Binding或了解基本概念。建议你直接查看Google文档,会帮助你轻松集成,有大量的示例代码,当你决定应用它到你的工程你可以实现很多很酷的东西。体验之后,你可能会好奇它是如何实现的,Google是怎样让传统的xml文件和data binding混合的,xml文件是怎样和java代码交互的,编译器是怎样处理的。本文主题旨在揭露data binding的机制,深入底层看看到底发生了什么。因此,开发者可以深入理解帮助他们正确地使用data binding,利用data binding的强大构建完美的应用。
近来,data binding是android很火的趋势,让开发者敲代码时更轻松。由于它的强大大量的开发者已经开始使用data binding。但说实话,它也带给我们一些麻烦。虽然data binding的概念很简单:“Yay!定义一个ViewModel类在 layout.xml 文件中引入并且不需要关注任何UI的东西,我们的数据会直接绑定到UI上用一种神奇的方式”。真的很快,很简单。当出现一些错误,当你的数据突然不能绑定到UI上,当编译器报出大量的错误信息,你真的不想知道这是什么意思么。我们能做些什么!
我不得不去面对这些data binding的问题并且使用不同的办法去解决它。并且我认为,只有通过查看data binding的源码,理解它工作的原理,我就不用再去处理这些问题了。让我们从Google clone下这个仓库一起阅读下它的代码Data-Binding Repository。
Part 1:Data Binding 流,Obsevable模式机制和data-binding生成代码的意思
为了能更好的理解代码,我创建了一个简单的使用data-binding的示例。
1.创建一个布局文件R.layout.activity_main
1 | xml |
2.创建MainActivity1
2
3
4
5
6
7
8
9
10
11
12
13java
public class MainActivity extends AppCompatActivity {
@Inject
MainViewModel mViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding bind = DataBindingUtil.setContentView(this, R.layout.activity_main);
bind.setViewModel(mViewModel);
}
}
3.创建MainViewModel1
2
3
4
5
6
7
8
9java
public class MainViewModel extends BaseObservable {
public final ObservableField<String> text = new ObservableField<>();
public MainViewModel() {
}
}
编译工程之后,我们可以看到data-binding生成的新的文件:
1.activity_main-layout.xml(在data-binding-info文件夹中)
1 | xml |
2.activity_main.xml(正常的布局文件)的简洁版本(在data-binding-layout-out文件夹中)1
2
3
4
5
6xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="layout/activity_main_0" />
3.ActivityMainBinding.java文件,这是我们的主角,最神奇的地方。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167java
public class ActivityMainBinding extends android.databinding.ViewDataBinding {
private static final android.databinding.ViewDataBinding.IncludedLayouts sIncludes;
private static final android.util.SparseIntArray sViewsWithIds;
static {
sIncludes = null;
sViewsWithIds = null;
}
// views
private final android.widget.TextView mboundView0;
// variables
private com.example.main.MainViewModel mViewModel;
// values
// listeners
// Inverse Binding Event Handlers
public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root) {
super(bindingComponent, root, 2);
final Object[] bindings = mapBindings(bindingComponent, root, 1, sIncludes, sViewsWithIds);
this.mboundView0 = (android.widget.TextView) bindings[0];
this.mboundView0.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
@Override
public void invalidateAll() {
synchronized(this) {
mDirtyFlags = 0x4L;
}
requestRebind();
}
@Override
public boolean hasPendingBindings() {
synchronized(this) {
if (mDirtyFlags != 0) {
return true;
}
}
return false;
}
public boolean setVariable(int variableId, Object variable) {
switch(variableId) {
case BR.viewModel :
setViewModel((com.example.main.MainViewModel) variable);
return true;
}
return false;
}
public void setViewModel(com.example.main.MainViewModel viewModel) {
updateRegistration(0, viewModel);
this.mViewModel = viewModel;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.viewModel);
super.requestRebind();
}
public com.example.main.MainViewModel getViewModel() {
return mViewModel;
}
@Override
protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
switch (localFieldId) {
case 0 :
return onChangeViewModel((com.example.main.MainViewModel) object, fieldId);
case 1 :
return onChangeTextViewMode((android.databinding.ObservableField<java.lang.String>) object, fieldId);
}
return false;
}
private boolean onChangeViewModel(com.example.main.MainViewModel viewModel, int fieldId) {
switch (fieldId) {
case BR._all: {
synchronized(this) {
mDirtyFlags |= 0x1L;
}
return true;
}
}
return false;
}
private boolean onChangeTextViewMode(android.databinding.ObservableField<java.lang.String> textViewModel, int fieldId) {
switch (fieldId) {
case BR._all: {
synchronized(this) {
mDirtyFlags |= 0x2L;
}
return true;
}
}
return false;
}
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
com.example.main.MainViewModel viewModel = mViewModel;
java.lang.String textViewModel = null;
android.databinding.ObservableField<java.lang.String> textViewModel1 = null;
if ((dirtyFlags & 0x7L) != 0) {
if (viewModel != null) {
// read viewModel.text
textViewModel1 = viewModel.text;
}
updateRegistration(1, textViewModel1);
if (textViewModel1 != null) {
// read viewModel.text.get()
textViewModel = textViewModel1.get();
}
}
// batch finished
if ((dirtyFlags & 0x7L) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView0, textViewModel);
}
}
// Listener Stub Implementations
// callback impls
// dirty flag
private long mDirtyFlags = 0xffffffffffffffffL;
public static ActivityMainBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot) {
return inflate(inflater, root, attachToRoot, android.databinding.DataBindingUtil.getDefaultComponent());
}
public static ActivityMainBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot, android.databinding.DataBindingComponent bindingComponent) {
return android.databinding.DataBindingUtil.<ActivityMainBinding>inflate(inflater, com.harpacrista.R.layout.activity_main, root, attachToRoot, bindingComponent);
}
public static ActivityMainBinding inflate(android.view.LayoutInflater inflater) {
return inflate(inflater, android.databinding.DataBindingUtil.getDefaultComponent());
}
public static ActivityMainBinding inflate(android.view.LayoutInflater inflater, android.databinding.DataBindingComponent bindingComponent) {
return bind(inflater.inflate(com.harpacrista.R.layout.activity_main, null, false), bindingComponent);
}
public static ActivityMainBinding bind(android.view.View view) {
return bind(view, android.databinding.DataBindingUtil.getDefaultComponent());
}
public static ActivityMainBinding bind(android.view.View view, android.databinding.DataBindingComponent bindingComponent) {
if (!"layout/activity_main_0".equals(view.getTag())) {
throw new RuntimeException("view tag isn't correct on view:" + view.getTag());
}
return new ActivityMainBinding(bindingComponent, view);
}
/* flag mapping
flag 0 (0x1L): viewModel
flag 1 (0x2L): viewModel.text
flag 2 (0x3L): null
flag mapping end*/
//end
}
正如我们所看到的,data-binding从activity_main.xml文件帮我们生成了3个额外的文件:activity_main-layout.xml
,activity_main.xml
的简洁版本和ActivityMainBinding.java
。至此,我们对xml布局生成的代码有了一个初步的了解。
基本上,一个xml布局文件最外层的<layout></layout>
标签表示和正常布局文件之间的区别。如果一个正常的布局文件是直接用于android应用,放置在apk包中的res/layout文件夹中,那布局文件中最外层的<layout></layout>
标签则是被间接使用。编译器通过搜索应用的layout文件夹编译所有最外层为<layout></layout>
标签的布局文件为activity_main.xml(正常的布局文件)的简洁版本。这个版本的xml看起来就像我们没有使用data-binding的正常的布局文件。
因此,基本上,使用data-binding的布局文件就是正常布局文件的一个特殊的版本,data-binding给我们一些词汇和语法,通过这种方式强制它们重写正常的布局文件。Xml文件不能被Android框架理解,它只能被data-binding编译器理解,这让编译器知道什么地方从ViewModel有怎样的数据映射到View上。最后编译器把xml文件转换成可以打包到apk中的正常的布局文件,以及activity_main-layout.xml 和 ActivityMainBinding.java。
我们可以想象原始的布局文件包含两部分:正常部分和绑定部分。正常部分就像我们没有使用data-binding时写的布局文件。绑定部分用于帮助编译器生成java代码,它是一个在UI和数据之间很好的桥梁。
activity_main-layout.xml (在data-binding-info文件夹中)包含绑定部分。就像它的名字,这个文件是布局的绑定信息。我们再来回顾下这个文件。外层仍是<Layout>
标签,但和之前的不一样。
1 | xml |
layout=”activity_main”属性表示这个文件是acitivity_main.xml的绑定部分,当前存放在”/home/framgia/android/app/src/main/res/layout/activity_main.xml”。
在<Layout>
标签的里面,毫无疑问,有<Variables>
标签和<Imports>
标签。因为我们前面我们用到setViewModel,并且在布局文件中引用了一些类。
1 | xml |
它们也包含位置location信息。我不知道为什么编译器需要存储Variable/Import的位置信息,可能用于追溯或其它什么作用。
接下来是这个文件中最重要的部分,<Target>
标签告诉ViewModel应该映射到哪个View,绑定类型为单向还是双向,View的位置及View值的位置。1
2
3
4
5
6
7
8
9
10
11
12
13xml
<Targets>
<Target tag="layout/activity_main_0" view="TextView">
<Expressions>
<Expression attribute="android:text" text=" viewModel.text ">
<Location endLine="16" endOffset="41" startLine="16" startOffset="8" />
<TwoWay>false</TwoWay>
<ValueLocation endLine="16" endOffset="39" startLine="16" startOffset="24" />
</Expression>
</Expressions>
<location endLine="16" endOffset="44" startLine="14" startOffset="4" />
</Target>
</Targets>
我们有一个<Target>
标签的列表。在布局文件中我们可以有多个View/ViewGroup,但只有包含data-binding表达式的View/ViewGroup才会出现在这里。<Target>
标签里面的<Expressions>
标签表示View的data-binding表达式。例如:
Data-binding表达式
android:visibility="@{ viewModel.isVisible ? View.VISIBLE : View.INVISIBLE }"
会被编译成1
2
3
4
5
6<Expression attribute="android:visibility"
text=" viewModel.isVisible ? View.VISIBLE : View.INVISIBLE ">
<Location endLine="29" endOffset="88" startLine="29" startOffset="12" />
<TwoWay>false</TwoWay>
<ValueLocation endLine="29" endOffset="86" startLine="29" startOffset="34" />
</Expression>Data-binding表达式
android:text="@{ viewModel.text }"
会被编译成1
2
3
4
5<Expression attribute="android:text" text=" viewModel.text ">
<Location endLine="23" endOffset="45" startLine="23" startOffset="12" />
<TwoWay>false</TwoWay>
<ValueLocation endLine="23" endOffset="43" startLine="23" startOffset="28" />
</Expression>
注意<Expression attribute="android:text" text=" viewModel.text ">
。它正是我们要找的View和ViewModel之间的桥梁。ViewModel中的text变量会连接到TextView的android:text属性。这正是我们所期待的。但从这个布局文件到java代码仍是一个谜。接下来看下ActivityMainBinding.java,一探究竟。
ActivityMainBinding.java是ViewDataBinding的一个子类,开发者可以setViewModel到布局文件。从我们分析的过程来看它就像xml的java版本。布局文件中的每个View/ViewGroup标签在这个类是一个变量。例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@{ viewModel.text }"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{ viewModel.isVisible ? View.VISIBLE : View.INVISIBLE }"
/>
</LinearLayout>
将会生成:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15java
public class ActivityMainBinding extends android.databinding.ViewDataBinding {
private static final android.databinding.ViewDataBinding.IncludedLayouts sIncludes;
private static final android.util.SparseIntArray sViewsWithIds;
static {
sIncludes = null;
sViewsWithIds = null;
}
// views
private final android.widget.LinearLayout mboundView0;
private final android.widget.TextView mboundView1;
private final android.widget.Button mboundView2;
...
}
并且如果你在布局文件中为View指定一个ID,这个private field将变成 public field,并且你可以直接使用它不用调用findViewById()。
1 | java |
这用起来很方便!访问布局文件中的任意View比之前更为容易。变量的名称和android:id的名称一致。如果android:id 带有下划线,名称将转换为驼峰式,例如android:id="@+id/tv_test"
会转换为public final android.widget.TextView tvTest;
。
<variable>
标签定义了java类中的变量,我们通过setViewModel方法设置的变量。顺便说一句,当我们讨论ViewModel时我想提到Obsevable模式。Obsevable模式是一个很关键的东西,无论ViewModel何时被修改都可以让View得到更新,并且ViewModel会自动接收一个新数据当用户和View交互时。这是所有的Obsevable类型覆盖了所有java数据类型。
1 | ObservableArrayList |
这些类的集合很相似也很简单。全部继承自BaseObservable,只包含一个变量,一个getter和一个setter。setter会检查新值和老值是否不同,如果是新值会调用notifyChange(),并且View会得到更新。但notifyChange()是怎样影响到View的?
为了回答上面的问题,我会使用这个例子尽可能简单地解释它:
- 期望: 我们修改ViewModel中的isShowView为false,actiivty_main.xml中的View将不可见。
- 里面发生了什么: 当我们setViewModel到ActivityMainBinding,updateRegistration被调用用于在ViewModel 和View之间创建一个“桥”。
我们来看下“桥”到底长什么样子:
1 | java |
WeakPropertyListener
中的mListener变量持有MainDataBinding
的一个弱引用,WeakListener
中的setTarget
方法被调用绑定ViewModel
到MainDataBinding
。- 从
ViewModel
修改isShowView
变量会调用mNotifier
的notifyCallback():
1 | java |
并且
mNotifier
也是WeakPropertyListener
。因此在ViewModel
上修改isShowView
将会通知WeakPropertyListener
,这是isShowView
影响View让它不可见的地方。ActivityMainBinding
中的executeBindings()
拿到值映射到UI上相应的View。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69java
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
com.example.main.MainViewModel viewModel = mViewModel;
int isVisibleViewModelVI = 0;
boolean isVisibleViewModel = false;
java.lang.String textViewModel = null;
android.databinding.ObservableBoolean isVisibleViewModel1 = null;
android.databinding.ObservableField<java.lang.String> textViewModel1 = null;
if ((dirtyFlags & 0xfL) != 0) {
if ((dirtyFlags & 0xbL) != 0) {
if (viewModel != null) {
// read viewModel.isVisible
isVisibleViewModel1 = viewModel.isVisible;
}
updateRegistration(1, isVisibleViewModel1);
if (isVisibleViewModel1 != null) {
// read viewModel.isVisible.get()
isVisibleViewModel = isVisibleViewModel1.get();
}
if((dirtyFlags & 0xbL) != 0) {
if (isVisibleViewModel) {
dirtyFlags |= 0x20L;
} else {
dirtyFlags |= 0x10L;
}}
// read viewModel.isVisible.get() ? View.VISIBLE : View.INVISIBLE
isVisibleViewModelVI = (isVisibleViewModel) ? (android.view.View.VISIBLE) : (android.view.View.INVISIBLE);
}
if ((dirtyFlags & 0xdL) != 0) {
if (viewModel != null) {
// read viewModel.text
textViewModel1 = viewModel.text;
}
updateRegistration(2, textViewModel1);
if (textViewModel1 != null) {
// read viewModel.text.get()
textViewModel = textViewModel1.get();
}
}
}
// batch finished
if ((dirtyFlags & 0xbL) != 0) {
// api target 1
this.mboundView2.setVisibility(isVisibleViewModelVI);
}
if ((dirtyFlags & 0xdL) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.tvTest, textViewModel);
}
}我们可以看到上面的代码,无论ViewModel何时修改它们的值,它都会通知
executeBindings()
方法,这个方法依赖于这个值并且会在View上执行动作,例如:1
2this.mboundView2.setVisibility(isVisibleViewModelVI);
android.databinding.adapters.TextViewBindingAdapter.setText(this.tvTest, textViewModel);这是为啥我们需要写@BindingAdapter和@BindingMethod用于在View上执行一个动作,例如:recyclerView.setAdapter(), viewPager.setOnPageChange(), …。
- 然而,Android已经创建了大量的内置BindingAdapters和BindingMethod,因此开发者大多数情况下只需要使用它。我们只需要在特殊情况下实现我们自己的代码或Android现在还不支持它。这种情况下,
isShowView
为false对应View.INVISIBLE会执行View.setVisibility(View.INVISIBLE)。That’s all。 - 进行下一部分之前,我想让你知道data-binding内置的adapters。记得使用这些并且不要重复发明轮子。我已经看到有很多开发者滥用@BindingAdapter重写已存在的东西。真是浪费!
Part 2:data-binding编译器是怎样生成代码的?
你有没有把我上面所给出的官方git仓库clone下来?Data-Binding Repository
在理解了生成的代码是怎样在View和ViewModel之间绑定之后,在这一部分,我们会找出编译生成神奇代码的方法。
注意这两个模块:compiler
和compilerCommon
。我们看的最多的地方。
- 编译器的核心为
compiler.android.databinding.annotationprocessor
包下的ProcessDataBinding
类。这个类的职责是一步一步执行处理列表。
1 | java |
- 我们先看第一个处理步骤——ProcessMethodAdapters。这个类提供搜索工程中所有的类,哪一个类哪一个方法添加了下面的注解:
@BindingAdapter, @Untaggable, @BindingMethods, @BindingConversion, @InverseBindingAdapter, @InverseBindingMethods
。并且把它们保存在SetterStore,后面应该在executeBinding用到正如我们上面所说。在编译期间,注解处理器拿到的这些信息会被存放在setter_store.bin
文件中。
1 | java |
- 第二步是ProcessExpressions处理表达式。在这一步中会搜索工程中所有xml文件并且会转换最外层为
<layout></layout>
标签支持data-binding的xml文件。会把这个文件拆分为2个文件正如第一部分所提到的:activity_main.xml(正常的布局文件)和activity_main-layout.xml(包含绑定信息)。LayoutBinder
是最有意思的类,它使用(XmlParser中)layoutBundle在activity_main-layout.xml
中计算表达式,位置和目标。
1 | java |
1 | java |
- 第三步是ProcessBindable。这个处理生成BR类,绑定属性的id,例如:BR.text, BR.item, BR.isShowView, …
- 最后,你可能想知道最重要的类MainDataBinding是在哪里创建的。我不知道为啥Google使用Kotlin编写的,相关文件为DataBinderWriter.kt 和 LayoutBinderWriter.kt。你可以自己去看这些文件。
总结
我希望你能读到这里,因为这篇文章有难点我们读起来很难理解。Data-Binding用起来不是很容易甚至很难理解。但我认为当我们真正理解后面的代码,没有什么是秘密,因为我们知道data-binding的原理。在一些工程上使用data-binding之后,我看到一些data-binding相关的bug很难跟踪和解决,开发者在它上面花费了很多时间。通过这篇文章,我们不再害怕深入到生成的代码查找bug原因。了解了编译器的知识可以帮助我们用最好的方式去写代码。
在下面留下评论,告诉我哪一块是你不理解的,我会尽力更新,让这篇文章更有用。Thanks for your time!