作者:codelang
ViewBinding 的本质
今天我们来深入的了解 ViewBinding 的本质,看看他是怎么生成 ActivityMainBinding 这种文件的。
使用
ViewBinding 目前只支持 as3.6,使用方法很简单,仅仅只需要添加如下代码:
android {
viewBinding {
enabled = true
}
}
make project 之后,会在对应的 module 路径:
app/build/generated/data_binding_base_class_source_out/${buildTypes}/out/${包名}/databinding
生成 ViewBinding 文件,为什么我会说 对应的 module ?因为 viewBinding 只对当前设置了 enabled = true 的 module 才会进行处理。
然后来看下处理后的文件:
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
@NonNull
public final Button tv;
private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull Button tv) {
this.rootView = rootView;
this.tv = tv;
}
@Override
@NonNull
public ConstraintLayout 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.
String missingId;
missingId: {
Button tv = rootView.findViewById(R.id.tv);
if (tv == null) {
missingId = "tv";
break missingId;
}
return new ActivityMainBinding((ConstraintLayout) rootView, tv);
}
...
}
}
我们来看看这个文件有哪些信息:
- R.layout.activity_main 布局文件
- 布局文件中的 view 控件和 view id
- 布局文件的 rootView 和类型
接下来,我们会通过源码的方式来跟踪到,这些信息是怎么产生的。
准备
由于我们并没有依赖其他 plugin 就可以使用,所以能被直接识别只能是 classpath 依赖的 gradle 了:
classpath 'com.android.tools.build:gradle:3.6.1'
既然 make project 之后就可以看到 ViewBinding 的生成类,那么,我们可以根据 make project 的 build 信息查看做了哪些 task:
> Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE
> Task :app:dataBindingMergeGenClassesDebug UP-TO-DATE
> ...
> Task :app:dataBindingGenBaseClassesDebug
没有找到 ViewBinding,但找到了 dataBinding,但可以肯定的是,这个 dataBinding 就是生成 ViewBinding 的 task(因为没有其他的 task 带有 binding)。然后我们可以去 maven 仓库找一下 gradle:3.6.1 ,惊喜的是,gradle:3.6.1 的依赖项有 18 个,第一个就是 Data Binding Compiler Common:
然后我们进去找到对应的 compiler 3.6.1 版本,通过 gradle 依赖,我们就能看到源码了:
compile group: 'androidx.databinding', name: 'databinding-compiler-common', version: '3.6.1'
可以看到,ViewBinding 是属于 dataBinding 库里面的一个小功能。
阶段一:收集元素
由于我们仅仅只是查看 dataBinding compiler,所以,对于 gradle 调用 compiler 的哪个部分进行联结,我们是查看不到的,但这也不影响我们跟踪源码。我们直接来看 :
LayoutXmlProcessor.java
public boolean processResources(final ResourceInput input, boolean isViewBindingEnabled) throws ParserConfigurationException, SAXException, XPathExpressionException,IOException
{
...
// 文件处理 callback
ProcessFileCallback callback = new ProcessFileCallback(){
...
// 是否是增量编译
if (input.isIncremental()) {
// 增量编译文件处理
processIncrementalInputFiles(input, callback);
} else {
// 全量编译文件处理
processAllInputFiles(input, callback);
}
...
}
我们直接来看 全量编译文件处理 :
private static void processAllInputFiles(ResourceInput input, ProcessFileCallback callback)throws IOException, XPathExpressionException, SAXException, ParserConfigurationException {
...
for (File firstLevel : input.getRootInputFolder().listFiles()) {
if (firstLevel.isDirectory()) {
// ①、判断 firstLevel.getName() 的 startWith 是否为 layout
if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
// ②、创建 subPath
callback.processLayoutFolder(firstLevel);
// ③、遍历 firstLevel 目录下面的所有文件,满足 toLowerCase().endsWith(".xml");
for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) {
// ④、处理布局文件
callback.processLayoutFile(xmlFile);
}
} else {
...
}
①、判断当前的文件夹的文件名 startWith 是否是 layout
②、会创建一个文件输出目录, 输出目录为 new File(input.getRootOutputFolder(),file path); 这个 file path 做了与输入目录的 relativize 化,其实,可以理解为,这个输出目录为 输出目录 + file 文件名 。
③、判断 layout 下面的文件名 endWith 是否是 .xml
④、处理 xml 文件,这个地方也会创建一个输出目录,跟 ② 的方式一样,最终,这个方法会调用到 processSingleFile 方法
然后我们来看下 processSingleFile 方法:
public boolean processSingleFile(@NonNull RelativizableFile input, @NonNull File output,boolean isViewBindingEnabled) throws ParserConfigurationException, SAXException, XPathExpressionException,IOException {
// ①、解析 xml
final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser
.parseXml(input, output, mResourceBundle.getAppPackage(), mOriginalFileLookup,
isViewBindingEnabled);
...
// ②、缓存起来
mResourceBundle.addLayoutBundle(bindingLayout, true);
return true;
}
①、这个地方会拿着 xml 文件的路径和输出路径进行解析
②、将解析结果缓存起来然后来看下 xml 的解析 parseXml
LayoutFileParser.java
@Nullable
public static ResourceBundle.LayoutFileBundle parseXml(@NonNull final RelativizableFile input,
@NonNull final File outputFile, @NonNull final String pkg,
@NonNull final LayoutXmlProcessor.OriginalFileLookup originalFileLookup,
boolean isViewBindingEnabled){
...
return parseOriginalXml(
RelativizableFile.fromAbsoluteFile(originalFile, input.getBaseDir()),
pkg, encoding, isViewBindingEnabled);
}
parseOriginalXml:
private static ResourceBundle.LayoutFileBundle parseOriginalXml(
@NonNull final RelativizableFile originalFile, @NonNull final String pkg,
@NonNull final String encoding, boolean isViewBindingEnabled)
throws IOException {
...
// ①、是否是 databinding
if (isBindingData) {
data = getDataNode(root);
rootView = getViewNode(original, root);
} else if (isViewBindingEnabled) {
// ②、viewBinding 是否开启
data = null;
rootView = root;// xml 的根元素
} else {
return null;
}
...
// 生成 bundle
ResourceBundle.LayoutFileBundle bundle =
new ResourceBundle.LayoutFileBundle(
originalFile, xmlNoExtension, original.getParentFile().getName(), pkg,
isMerge, isBindingData, getViewName(rootView));
final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;
// viewBinding 不会 解析 data
parseData(original, data, bundle);
// ③、解析表达式
parseExpressions(newTag, rootView, isMerge, bundle);
return bundle;
①、是否是 databinding,这个的判断依据是,根元素是否是 layout , 获取 data 和 rootView
②、isViewBindingEnable 就是 gradle 设置的 enable = true,根元素就是就是他的 rootView,这个地方要注意的是 data = null,data 数据只有 databinding 才会有的元素,viewBinding 是不会去解析的
③、解析表达式,这里面会循环遍历元素,解析 view 的 id、tag、include、fragment 等等 xml 相关的元素,并且还有 databinding 相关的 @={ 的表达式,最后将结果缓存起来,源码我就补贴了,太多,影响文章
阶段二:写 Layout 文件
LayoutXmlProcessor.java
// xml 的输出目录
public void writeLayoutInfoFiles(File xmlOutDir) throws JAXBException {
writeLayoutInfoFiles(xmlOutDir, mFileWriter);
}
public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {
// ①、遍历收集的 layout file
for (ResourceBundle.LayoutFileBundle layout : mResourceBundle
.getAllLayoutFileBundlesInSource()) {
writeXmlFile(writer, xmlOutDir, layout);
}
...
}
private void writeXmlFile(JavaFileWriter writer, File xmlOutDir,ResourceBundle.LayoutFileBundle layout)throws JAXBException {
// ②、生成文件名
String filename = generateExportFileName(layout);
// ③、写文件
writer.writeToFile(new File(xmlOutDir, filename), layout.toXML());
}
①、遍历之前收集到的所有 LayoutFileBundle,写入 xmlOutDir 路径
②、生成 LayoutFileBundle 的文件名,这个文件名最终生成为:
layout.getFileName() + '-' + layout.getDirectory() + ".xml
例如 activity_main.xml,生成的 fileName 为 activity_main-layout.xml
③、将 LayoutFileBundle 转换 xml ,写入文件
由于我们是直接跟踪的 databinding compiler 库,所以无法跟踪到 gradle 是什么联结 compiler 库的,所以,xmlOutDir 我是未知的,也不知道他存到了哪,但没有关系,我们既然知道了生成的文件名规则,我们可以全局搜索该文件,最终,我们在该目录中搜索到:
app/build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_main-layout.xml
文件内容如下:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="/Users/codelang/project/app/src/main/res/layout/activity_main.xml"
isBindingData="false"
isMerge="false" layout="activity_main" modulePackage="com.codelang.viewBinding"
rootNodeType="androidx.constraintlayout.widget.ConstraintLayout">
<Targets>
<Target tag="layout/activity_main_0"
view="androidx.constraintlayout.widget.ConstraintLayout">
<Expressions />
<location endLine="31" endOffset="51" startLine="1" startOffset="0" />
</Target>
<Target id="@+id/tv" view="Button">
<Expressions />
<location endLine="16" endOffset="51" startLine="8" startOffset="4" />
</Target>
</Targets>
</Layout>
这份 xml 描述了原始 layout 的相关信息,对于 include 和 merge 是怎么关联 tag 的,读者可以自行运行查看
阶段三:写 ViewBinding 类
BaseDataBinder.java
@Suppress("unused")// used by tools
class BaseDataBinder(val input : LayoutInfoInput) {
init {
input.filesToConsider.forEach {
it.inputStream().use {
// 又将上面收集的 layout,将 xml 转成 LayoutFileBundle
val bundle = LayoutFileBundle.fromXML(it)
// 缓存进 ResourceBundle
resourceBundle.addLayoutBundle(bundle, true)
}
}
...
}
可以看到,最后又去读之前生成的 layout xml,这个地方为什么会又写又读,而不是直接利用之前 layout 的缓存?我想可能是因为解耦,他们都是独立的 task。
然后来看是如何生成 Binding 类的:
@Suppress("unused")// used by android gradle plugin
fun generateAll(writer : JavaFileWriter) {
// 拿到所有的 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 处理
val viewBinder = layoutModel.toViewBinder()
// ②、生成 java file 文件
javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
...
}
writer.writeToFile(javaFile)
...
}
①、toViewBinder 是 BaseLayoutModel 的拓展函数,他会将 LayoutFileBundle 包装成 ViewBinder 类返回
②、toJavaFile 是 ViewBinder 的拓展函数,该拓展函数在 ViewBinderGenerateSource 类中
ViewBinderGenerateSource.java
// ①、最终会调用到 JavaFileGenerator 的 create 方法
fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
JavaFileGenerator(this, useLegacyAnnotations).create()
private class JavaFileGenerator(
private val binder: ViewBinder,
private val useLegacyAnnotations: Boolean) {
// 最终会调用生成 javaFile 方法,生成的类信息主要看 typeSpec 方法
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"))
// TODO determine if we can elide the separate root field if the root tag has an ID.
addField(rootViewField())
addFields(bindingFields())
addMethod(constructor())
addMethod(rootViewGetter())
if (binder.rootNode is RootNode.Merge) {
addMethod(mergeInflate())
} else {
addMethod(oneParamInflate())
addMethod(threeParamInflate())
}
addMethod(bind())
}
这个地方就贴 typeSpec 方法了,具体的,大家可以自己去看源码,从 typeSpec 中,我们就可以看到点生成的 ViewBinding 类包含了哪些东西,rootView 字段,inflater 、bind 方法。
总结
文章已经尽量保持源码简短,只贴核心部分。本来还想絮絮叨叨一下,算了,就到这
当然了,本文所列出的知识点还不完全,要比较系统的学习,我这里可以分享我分享一份大佬收录整理的Android学习PDF+架构视频+面试文档+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料
这些都是我现在闲暇还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效的帮助大家掌握知识、理解原理。
当然你也可以拿去查漏补缺,提升自身的竞争力。
如果你有需要,可以评论或者私信获取
如果你觉得自己学习效率低,缺乏正确的指导,可以加入资源丰富,学习氛围浓厚的技术圈一起学习交流吧!
喜欢本文的话,不妨顺手给我点个赞、评论区留言或者转发支持一下呗~
本文暂时没有评论,来添加一个吧(●'◡'●)