终于有一篇带大家从本质来了解 ViewBinding 了。

如果你完全没了解过 View Binding,可以先看下面这篇介绍:

AS 3.6 Canary 中推出新技术 视图绑定 View Binding

今天我们来深入的了解 ViewBinding 的本质,看看他是怎么生成 ActivityMainBinding 这种文件的。

1、使用

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 {@NonNullprivate final ConstraintLayout rootView;@NonNullpublic final Button tv;private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull Button tv) {this.rootView = rootView;this.tv = tv;}@Override@NonNullpublic ConstraintLayout getRoot() {return rootView;}@NonNullpublic static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {return inflate(inflater, null, false);}@NonNullpublic 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);}@NonNullpublic 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 和类型

接下来,我们会通过源码的方式来跟踪到,这些信息是怎么产生的。

2 、准备

由于我们并没有依赖其他 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:

https://mvnrepository.com/artifact/com.android.tools.build/gradle/3.6.1​mvnrepository.com

然后我们进去找到对应的 compiler 3.6.1 版本,通过 gradle 依赖,我们就能看到源码了:

https://mvnrepository.com/artifact/androidx.databinding/databinding-compiler-common/3.6.1

compile group: 'androidx.databinding', name: 'databinding-compiler-common', version: '3.6.1'

可以看到,ViewBinding 是属于 dataBinding 库里面的一个小功能。

3 阶段一:收集元素

由于我们仅仅只是查看 dataBinding compiler,所以,对于 gradle 调用 compiler 的哪个部分进行联结,我们是查看不到的,但这也不影响我们跟踪源码。

public boolean processResources(final ResourceInput input, boolean isViewBindingEnabled) throws ParserConfigurationException, SAXException, XPathExpressionException,IOException
{...// 文件处理 callbackProcessFileCallback 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 是否为 layoutif (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 {// ①、解析 xmlfinal ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser.parseXml(input, output, mResourceBundle.getAppPackage(), mOriginalFileLookup,isViewBindingEnabled);...// ②、缓存起来mResourceBundle.addLayoutBundle(bindingLayout, true);return true;
}

①、这个地方会拿着 xml 文件的路径和输出路径进行解析
②、将解析结果缓存起来

然后来看下 xml 的解析 parseXml

 @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 {...// ①、是否是 databindingif (isBindingData) {data = getDataNode(root);rootView = getViewNode(original, root); } else if (isViewBindingEnabled) { // ②、viewBinding 是否开启data = null;rootView = root;// xml 的根元素} else {return null;}...// 生成 bundleResourceBundle.LayoutFileBundle bundle =new ResourceBundle.LayoutFileBundle(originalFile, xmlNoExtension, original.getParentFile().getName(), pkg,isMerge, isBindingData, getViewName(rootView));final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;// viewBinding 不会 解析 dataparseData(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 相关的 @={ 的表达式,最后将结果缓存起来,源码我就补贴了,太多,影响文章

4 、阶段二:写 Layout 文件

// xml 的输出目录
public void writeLayoutInfoFiles(File xmlOutDir) throws JAXBException {writeLayoutInfoFiles(xmlOutDir, mFileWriter);
}
public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {// ①、遍历收集的 layout filefor (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 的,读者可以自行运行查看

5 、阶段三:写 ViewBinding 类

@Suppress("unused")// used by tools
class BaseDataBinder(val input : LayoutInfoInput) {init {input.filesToConsider.forEach {it.inputStream().use {// 又将上面收集的 layout,将 xml 转成 LayoutFileBundleval bundle = LayoutFileBundle.fromXML(it)// 缓存进 ResourceBundleresourceBundle.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)// 遍历 layoutBindingslayoutBindings.forEach { layoutName, variations ->// 将 LayoutFileBundle 信息包装成 BaseLayoutModelval layoutModel = BaseLayoutModel(variations)val javaFile: JavaFileval classInfo: GenClassInfoLog.GenClass// 当前是否是 databindingif (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 类中

// ①、最终会调用到 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 方法。

总结

文章已经尽量保持源码简短,只贴核心部分。本来还想絮絮叨叨一下,算了,就到这。

文章有关参考

https://github.com/AndroidCot/Android​github.com

android viewbinding_程序员必懂小技巧之ViewBinding相关推荐

  1. 师妹问我:有哪些新手程序员不知道的小技巧?

    阅读本文大概需要5分钟. 一个师妹问:洋哥,我今年应届毕业,刚开始写代码,不知道有没有一些新手需要注意的地方. 给了师妹一些建议之后,感觉这是个好问题!不光是新手程序员,很多小技巧小秘密恐怕老手也未必 ...

  2. 程序员装B小技巧——管理你的桌面

    程序员装B小技巧--管理你的桌面 引言 想不想拥有一个和下方截图一样简单快捷的桌面?随着工作学习的时间推移,我们电脑里面的各种软件和文档日渐增多,导致我们的桌面变得杂乱且不美观.身为一个程序员怎么能容 ...

  3. 专业程序员必知的技巧:敲打代码

    文/ Josh Myself to 40's http://www.embassyofperu.org/ products investment the using cheap canadian vi ...

  4. 你想被开除吗?来看看程序员【离职小技巧】吧

    梦想橡皮擦,一个逗趣的互联网高级网虫. 文章目录 写在前面 1. 熟练使用 rm 命令 2. 读写数据库操作都写在 for 循环中 3. 永远不写注释,不封装代码 4. git 上面强制合并代码 5. ...

  5. 那些程序员必懂的行业“黑话”——详细总结(包括招聘行话,职场行话,公司行话,移动互联网行话)

    前言 今天总结一下互联网行业招聘时的一些术语(俗称招聘黑话)和移动互联网的一些黑话吧. 防止自己以后求职时听不懂对方真正想表达的意思(笑哭). 一.HR招聘黑话 1.能承受较大的工作压力--加班 2. ...

  6. 优秀程序员必懂知识点,你要是还不会就out了

    进行社招面试时,有一个问题几乎是必问的: 你为什么要离开上一家公司? 其实这个问题主要是想试探一下求职者的核心诉求,并借此预估一下他在本公司工作的稳定性.常见的答案也无非就是这么几种:对薪酬不满意.干 ...

  7. 程序员必懂的程序设计原则

    来自:source: //bigjun2017.github.io/2018/11/24/ruan-jian-she-ji/ruan-jian-cheng-xu-she-ji-yuan-ze 一.前言 ...

  8. 有哪些新手程序员不知道的小技巧?

    提到新手程序员,大家想到的第一个词可能就是:刷题.尤其是通过LeetCode刷题,想必新手程序员们都经历过这一步,甚至不少人认为只要在LeetCode上刷的题目够多,就一定能够进阶为大神. 但是,不难 ...

  9. 新手程序员不知道的小技巧!

    1.作为前端开发者,使用双显示器能大幅提高开发效率. 2.学编程最好的语言不是PHP,是English. 3.东西交付之前偷偷测试一遍. 4.问别人之前最好先自己百度,google一下,以免问出太低级 ...

最新文章

  1. vba与MySQL交互_Excel、VBA与MySQL交互
  2. 使用Elasticsearch+filebeat+logstach+kibana构建日志服务平台
  3. 恒安嘉新面试题java_面了三个大厂,终于拿到offer,数年Java最经典的面试题总结...
  4. AutoConfigurationImportSelector是什么?
  5. 【转】 Objective C实现多继承
  6. XML Schema 简介
  7. 前端每日实战:76# 视频演示如何用纯 CSS 创作一组单元素办公用品(内含2个视频)...
  8. python数据可视化之美 豆瓣_Python数据可视化:豆瓣电影TOP250
  9. WinRAR 无广告注册安装
  10. 8-1 职场价值塑造-摆脱低价值瓶颈,展示高价值收获新机会
  11. html头像生成器,Personas – 免费的卡通风格头像生成器
  12. 怎样用计算机xp命令修复软件,系统之家xp系统修复控制台命令使用方法
  13. 解决vuecli-vue2项目ie浏览器白屏
  14. 数据库标准语言SQL(六)——单表查询(二)
  15. threejs基础下:贴图资源加载与光照
  16. p73 应急响应-WEB 分析 phpjavaweb自动化工具
  17. TSINGSEE青犀视频RTMP推流摄像头焦距与监控距离存在什么关系?
  18. k8s 介绍与基本使用
  19. IDEA迁移到其他电脑,直接复制文件夹
  20. coursera andrew Ng老师的machine learning的课程总结(一)

热门文章

  1. Zabbix监控Mysql数据库性能
  2. Go报错package github.com/astaxie/beego: exit status 128
  3. 快速计算文件的MD5/SHA1/SHA256等校验值(Windows/Linux)
  4. 开卡教程_流量卡开卡教程(必看)
  5. 安徽大学计算机教学平台c语言作业,安徽大学计算机基础C语言选择题
  6. mysql计算本月的天数_Mysql已知年、月,求起始日期,本月天数
  7. qtextedit非编辑时去边框_Photoshop玩腻了!这10个图形编辑神器你知道吗
  8. c语言作业做出金山打字功能,goldmountain.c
  9. 联想拯救者y空间兑换代码_十代酷睿全面升级 拯救者Y7000P 2020产品解读
  10. mysql删除重复sql_mysql中删除完全重复数据的准确SQL语句