Android Jetpack 之 ViewBinding 源码分析

在 Kotlin 1.4.20-M2 中,JetBrains 废弃了 Kotlin Android Extensions 编译插件。

不要与 Data Binding 混淆。View Binding 是一种功能,它允许您更容易地编写与视图交互的代码。
一旦在一个模块中启用了 View Binding,它就会为该模块中存在的每个 XML 布局文件生成一个绑定类。

  • 绑定类的实例包含对相应布局中具有 ID 的所有 VIew 的直接引用。
  • View Binding 对于在多个配置中定义的布局来说是 Null-safe 的。
  • View Binding 将检测视图是否只存在于某些配置中,并创建一个@Nullable 属性。
  • View Binding 适用于 Java 和 Kotlin。

禁用某个布局文件使用 ViewBinding,则需要在布局文件的根节点中添加 tools:viewBindingIgnore = "true"

环境配置

  1. Android Studio 3.6 及以上;
  2. Android Gradle 插件 3.6.0 及以上;
  3. Gradle 版本 5.6.4 及以上;
  4. 在模块的 build.gradle 文件中添加 android { buildFeatures { viewBinding = true } }android { viewBinding.enabled = true } 方式已废弃。
    DSL element ‘android.viewBinding.enabled’ is obsolete and has been replaced with ‘android.buildFeatures.viewBinding’.

GradlePlugin 和 Gradle 要对应,否则会不兼容,出现同步失败的问题。查看 Android Gradle 插件版本说明。

使用

1
2
3
4
5
6
7
8
9
10
11
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.textView.text = "text"
}
}

Activity 对应的 activity_main.xml 文件如下:

1
2
3
4
5
6
7
8
9
10
11
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</LinearLayout>

生成的 ActivityMainBinding 在 module/build/generated/data_binding_base_class_source_out/${buildTypes}/out/${包名}/databinding 目录,源码如下:

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
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView;

@NonNull
public final TextView textView;

private ActivityMainBinding(@NonNull LinearLayout rootView, @NonNull TextView textView) {
this.rootView = rootView;
this.textView = textView;
}

@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}

@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}

@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent,
boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}

@NonNull
public static ActivityMainBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.textView;
TextView textView = rootView.findViewById(id);
if (textView == null) {
break missingId;
}

return new ActivityMainBinding((LinearLayout) rootView, textView);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}

Binding 类生成原理

由于 ViewBinding 类是通过 GradlePlugin 支持的,编译时包含有 DataBinding 相关的 task Task :app:dataBindingGenBaseClassesDebug

com.android.tools.build:gradle:4.1.3 依赖 androidx.databinding:databinding-compiler-common:4.1.3,下载 databinding-compiler-common jar 包,解压到文件夹后使用 AS 打开查看源码。

解析 xml 文件

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
/**
* Processes the layout XML, stripping the binding attributes and elements
* and writes the information into an annotated class file for the annotation
* processor to work with.
*/
public class LayoutXmlProcessor {
public boolean processResources(ResourceInput input, boolean isViewBindingEnabled,
boolean isDataBindingEnabled) {

ProcessFileCallback callback = new ProcessFileCallback() {
@Override
public void processLayoutFile(File file) {
processSingleFile(...);
}

// 其他回调方法
};

if (input.isIncremental()) {
processIncrementalInputFiles(input, callback);
} else {
processAllInputFiles(input, callback);
}
}

private static void processAllInputFiles(ResourceInput input, ProcessFileCallback callback) {
for (File firstLevel : input.getRootInputFolder().listFiles()) {
if (firstLevel.isDirectory()) {
// 判断文件名是否已 layout 开头
if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
callback.processLayoutFolder(firstLevel);
// 过滤以 “.xml” 结尾的文件
for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) {
callback.processLayoutFile(xmlFile);
}
}
}
}
}

public boolean processSingleFile(RelativizableFile input, File output,
boolean isViewBindingEnabled,
boolean isDataBindingEnabled) {
// 解析 xml
final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser.parseXml(input, output, ...);
// 缓存解析的 xml
mResourceBundle.addLayoutBundle(bindingLayout, true);
}
}
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
public final class LayoutFileParser {

public static ResourceBundle.LayoutFileBundle parseXml(final RelativizableFile input,
final File outputFile, ...) {
return parseOriginalXml(
RelativizableFile.fromAbsoluteFile(originalFile, input.getBaseDir()),...);
}

private static ResourceBundle.LayoutFileBundle parseOriginalXml(
final RelativizableFile originalFile, ...) {
File original = originalFile.getAbsoluteFile();
// 是否是 DataBinding
if (isBindingData) {
data = getDataNode(root);
rootView = getViewNode(original, root);
} else if (isViewBindingEnabled) {
// 排除不生成 Binding 类的 xml
if ("true".equalsIgnoreCase(attributeMap(root).get("tools:viewBindingIgnore"))) {
L.d("Ignoring %s for view binding", originalFile);
return null;
}
data = null;
rootView = root;
} else {
return null;
}

// 判断是否是 merge 节点
boolean isMerge = "merge".equals(rootView.elmName.getText());
if (isBindingData && isMerge && !filter(rootView, "include").isEmpty()) {
L.e(ErrorMessages.INCLUDE_INSIDE_MERGE);
return null;
}

String rootViewType = getViewName(rootView);
// 获取 View ID
String rootViewId = attributeMap(rootView).get("android:id");
// 创建 LayoutFileBundle 对象
ResourceBundle.LayoutFileBundle bundle =
new ResourceBundle.LayoutFileBundle(originalFile, ...);
// ViewBinding data == null,不会解析
parseData(original, data, bundle);
// 解析表达式
parseExpressions(newTag, rootView, isMerge, bundle);

return bundle;
}

private static String getViewName(XMLParser.ElementContext elm) {
String viewName = elm.elmName.getText();
if ("view".equals(viewName)) {
String classNode = attributeMap(elm).get("class");
if (Strings.isNullOrEmpty(classNode)) {
L.e("No class attribute for 'view' node");
}
return classNode;
}
if ("include".equals(viewName) && !XmlEditor.hasExpressionAttributes(elm)) {
return "android.view.View";
}
if ("fragment".equals(viewName)) {
return "android.view.View";
}
return viewName;
}
}

生成 xml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LayoutXmlProcessor {
public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) {
for (ResourceBundle.LayoutFileBundle layout : mResourceBundle
.getAllLayoutFileBundlesInSource()) {
writeXmlFile(writer, xmlOutDir, layout);
}
}

private void writeXmlFile(JavaFileWriter writer, File xmlOutDir,
ResourceBundle.LayoutFileBundle layout) {
// layout.getFileName() + '-' + layout.getDirectory() + ".xml
// 如 activity_main.xml -> activity_main-layout.xml
String filename = generateExportFileName(layout);
writer.writeToFile(new File(xmlOutDir, filename), layout.toXML());
}
}

生成的 xml 文件路径为 module/build/intermediates/data_binding_layout_info_type_merge/${buildTypes}/out/activity_main-layout.xml,描述了原始布局文件的相关信息。

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
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout
directory="layout"
filePath="app\src\main\res\layout\activity_main.xml"
isBindingData="false"
isMerge="false"
layout="activity_main"
modulePackage="com.king.app.workhelper"
rootNodeType="android.widget.LinearLayout">

<Targets>

<Target
tag="layout/activity_main_0"
view="LinearLayout">

<Expressions />

<location
endLine="11"
endOffset="14"
startLine="1"
startOffset="0" />
</Target>

<Target
id="@+id/textView"
view="TextView">

<Expressions />

<location
endLine="9"
endOffset="46"
startLine="6"
startOffset="4" />
</Target>
</Targets>
</Layout>

生成 Binding 类

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
class BaseDataBinder(val input : LayoutInfoInput) {
fun generateAll(writer : JavaFileWriter) {
input.invalidatedClasses.forEach {
writer.deleteFile(it)
}

val useAndroidX = input.args.useAndroidX
val libTypes = LibTypes(useAndroidX = useAndroidX)

// 获取所有的 LayoutFileBundle,并根据文件名进行分组排序
val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
.groupBy(LayoutFileBundle::getFileName)

// 遍历 layoutBindings
layoutBindings.forEach { layoutName, variations ->
// 将 LayoutFileBundle 信息包装成 BaseLayoutModel
val layoutModel = BaseLayoutModel(variations)

val javaFile: JavaFile
val classInfo: GenClassInfoLog.GenClass
// 处理 DataBinding
if (variations.first().isBindingData) {
//...
} else {
// 处理 ViewBinding
// 创建 ViewBinder 对象
val viewBinder = layoutModel.toViewBinder()
// 通过 ViewBinder 扩展方法,调用到 ViewBinderGenerateJava#create() 方法
javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
classInfo = viewBinder.generatedClassInfo()
}

writer.writeToFile(javaFile)
}
}
}
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
// ViewBinderGenerateJava.kt

fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
JavaFileGenerator(this, useLegacyAnnotations).create()

private class JavaFileGenerator(
private val binder: ViewBinder,
private val useLegacyAnnotations: Boolean) {

// 使用了 com.squareup.javapoet 库
fun create() = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
addFileComment("Generated by view binder compiler. Do not edit!")
}

private fun typeSpec() = classSpec(binder.generatedTypeName) {
addModifiers(PUBLIC, FINAL)

val viewBindingPackage = if (useLegacyAnnotations) "android" else "androidx"
addSuperinterface(ClassName.get("$viewBindingPackage.viewbinding", "ViewBinding"))

// 生成 rootView 字段
addField(rootViewField())
// 生成 View 字段
addFields(bindingFields())

// 生成构造方法
addMethod(constructor())
// 生成获取 rootView 的方法
addMethod(rootViewGetter())

if (binder.rootNode is RootNode.Merge) {
addMethod(mergeInflate())
} else {
// 生成 inflate(LayoutInflater inflater) 方法
addMethod(oneParamInflate())
// 生成 inflate(LayoutInflater inflater, ViewGroup parent, boolean attachToParent) 方法
addMethod(threeParamInflate())
}

addMethod(bind())
}
}
  1. 遍历收集的 Set<LayoutFileBundle>;
  2. 根据 LayoutFileBundle 生成 ViewBinder 对象;
  3. ViewBinder 传参给 JavaFileGenerator,通过 JavaPoet(com.squareup.javapoet) 库生成 Binding Class 文件。

参考

[1] ViewBinding - Developer
[2] 终于有人写:「新技术 ViewBinding」 的本质了
[3] [译]深入研究 ViewBinding 在 include, merge, adapter, fragment, activity 中使用
[4] hoc081098/ViewBindingDelegate