android viewbinding_程序员必懂小技巧之ViewBinding
终于有一篇带大家从本质来了解 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.1mvnrepository.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/Androidgithub.com
android viewbinding_程序员必懂小技巧之ViewBinding相关推荐
- 师妹问我:有哪些新手程序员不知道的小技巧?
阅读本文大概需要5分钟. 一个师妹问:洋哥,我今年应届毕业,刚开始写代码,不知道有没有一些新手需要注意的地方. 给了师妹一些建议之后,感觉这是个好问题!不光是新手程序员,很多小技巧小秘密恐怕老手也未必 ...
- 程序员装B小技巧——管理你的桌面
程序员装B小技巧--管理你的桌面 引言 想不想拥有一个和下方截图一样简单快捷的桌面?随着工作学习的时间推移,我们电脑里面的各种软件和文档日渐增多,导致我们的桌面变得杂乱且不美观.身为一个程序员怎么能容 ...
- 专业程序员必知的技巧:敲打代码
文/ Josh Myself to 40's http://www.embassyofperu.org/ products investment the using cheap canadian vi ...
- 你想被开除吗?来看看程序员【离职小技巧】吧
梦想橡皮擦,一个逗趣的互联网高级网虫. 文章目录 写在前面 1. 熟练使用 rm 命令 2. 读写数据库操作都写在 for 循环中 3. 永远不写注释,不封装代码 4. git 上面强制合并代码 5. ...
- 那些程序员必懂的行业“黑话”——详细总结(包括招聘行话,职场行话,公司行话,移动互联网行话)
前言 今天总结一下互联网行业招聘时的一些术语(俗称招聘黑话)和移动互联网的一些黑话吧. 防止自己以后求职时听不懂对方真正想表达的意思(笑哭). 一.HR招聘黑话 1.能承受较大的工作压力--加班 2. ...
- 优秀程序员必懂知识点,你要是还不会就out了
进行社招面试时,有一个问题几乎是必问的: 你为什么要离开上一家公司? 其实这个问题主要是想试探一下求职者的核心诉求,并借此预估一下他在本公司工作的稳定性.常见的答案也无非就是这么几种:对薪酬不满意.干 ...
- 程序员必懂的程序设计原则
来自:source: //bigjun2017.github.io/2018/11/24/ruan-jian-she-ji/ruan-jian-cheng-xu-she-ji-yuan-ze 一.前言 ...
- 有哪些新手程序员不知道的小技巧?
提到新手程序员,大家想到的第一个词可能就是:刷题.尤其是通过LeetCode刷题,想必新手程序员们都经历过这一步,甚至不少人认为只要在LeetCode上刷的题目够多,就一定能够进阶为大神. 但是,不难 ...
- 新手程序员不知道的小技巧!
1.作为前端开发者,使用双显示器能大幅提高开发效率. 2.学编程最好的语言不是PHP,是English. 3.东西交付之前偷偷测试一遍. 4.问别人之前最好先自己百度,google一下,以免问出太低级 ...
最新文章
- vba与MySQL交互_Excel、VBA与MySQL交互
- 使用Elasticsearch+filebeat+logstach+kibana构建日志服务平台
- 恒安嘉新面试题java_面了三个大厂,终于拿到offer,数年Java最经典的面试题总结...
- AutoConfigurationImportSelector是什么?
- 【转】 Objective C实现多继承
- XML Schema 简介
- 前端每日实战:76# 视频演示如何用纯 CSS 创作一组单元素办公用品(内含2个视频)...
- python数据可视化之美 豆瓣_Python数据可视化:豆瓣电影TOP250
- WinRAR 无广告注册安装
- 8-1 职场价值塑造-摆脱低价值瓶颈,展示高价值收获新机会
- html头像生成器,Personas – 免费的卡通风格头像生成器
- 怎样用计算机xp命令修复软件,系统之家xp系统修复控制台命令使用方法
- 解决vuecli-vue2项目ie浏览器白屏
- 数据库标准语言SQL(六)——单表查询(二)
- threejs基础下:贴图资源加载与光照
- p73 应急响应-WEB 分析 phpjavaweb自动化工具
- TSINGSEE青犀视频RTMP推流摄像头焦距与监控距离存在什么关系?
- k8s 介绍与基本使用
- IDEA迁移到其他电脑,直接复制文件夹
- coursera andrew Ng老师的machine learning的课程总结(一)
热门文章
- Zabbix监控Mysql数据库性能
- Go报错package github.com/astaxie/beego: exit status 128
- 快速计算文件的MD5/SHA1/SHA256等校验值(Windows/Linux)
- 开卡教程_流量卡开卡教程(必看)
- 安徽大学计算机教学平台c语言作业,安徽大学计算机基础C语言选择题
- mysql计算本月的天数_Mysql已知年、月,求起始日期,本月天数
- qtextedit非编辑时去边框_Photoshop玩腻了!这10个图形编辑神器你知道吗
- c语言作业做出金山打字功能,goldmountain.c
- 联想拯救者y空间兑换代码_十代酷睿全面升级 拯救者Y7000P 2020产品解读
- mysql删除重复sql_mysql中删除完全重复数据的准确SQL语句