Aspect 语法难懂?ASM 字节码操做繁琐?APT 难以精准找到切入点?你该试试 AST 了!编辑器级别,效率高,更轻量。html
在开始上手以前,咱们先了解下几个简单的概念:java
咱们知道,编程语言再怎么变,不变的是由「类型」「运算符」「流程语句」「函数」「对象」组成的本质,这些本质概念表达了底层的运算与逻辑,那么这么多编程语言,要怎么抽离出这个逻辑本质呢?android
答案就是:转化为统一的结构!git
这个统一的结构不依赖于源语言的语法,只表明源语言中的语法结构,如类型、修饰符、运算符…… 这就是抽象语法树 AST。AST(abstract syntax tree)即抽象语法树,是源代码的抽象语法结构的树状表现形式,每个节点表明一个语法结构。那 AST 是怎么转化得来的呢?github
不一样的语言,都会有对应不一样的语法分析器,语法分析器会把源代码做为字符串读入、解析,并创建语法树,这是一个程序完成编译所必要的前期工做。编程
咱们看下 Java 的编译过程,重点关注步骤一和步骤二:架构
步骤一:词法分析,将源代码的字符流转变为 Token 列表。app
一个个读取源代码,按照预约规则合并成 Token,Token 是编译过程的最小元素,关键字、变量名、字面量、运算符等均可以成为 Token。eclipse
步骤二:语法分析,根据 Token 流来构造树形表达式也就是 AST。编程语言
语法树的每个节点都表明着程序代码中的一个语法结构,如类型、修饰符、运算符等。通过这个步骤后,编译器就基本不会再对源码文件进行操做了,后续的操做都创建在抽象语法树之上。
- 能够访问 Astexplorer 在线玩转 AST
咱们能够发现,AST 定义了代码的结构,经过操做 AST,咱们能够精准地定位到声明语句、赋值语句、运算语句等,实现对源代码的分析、优化、变动等操做。
举个例子,想要改变 a 的赋值,以下图:
想改 a 的赋值,能够对 AST 语法树的 value 节点下手,一旦改动,编译器会从新进行编译流程处理,此时赋值改动就反映到源码上了。是否是很神奇?其实 Lombok、IDE 语法高亮、IDE 格式化代码、自动补全、代码混淆压缩、甚至大名鼎鼎的 ButterKnife 的 R、R2 文件映射和静态代码检查,都是利用了 AST。
既然要操做 AST,咱们怎么拿到 AST 呢?
答案是:在注解处理器 APT!
利用 JDK 的注解处理器,可在编译期间处理注解,还能够读取、修改、添加 AST 中的任意元素,让改动后的 AST 从新参与编译流程处理,直到语法树没有改动为止。
相比其余的AOP方法,AST 属于编辑器级别,时机更为提早,效率更高。
但语法复杂,推荐经过库来操做 AST:
总体思路:在编译期间拿到 AST,扫描是否含有特定日志语句如:Log,存在则删除该语句。
@SupportedAnnotationTypes
指定此注解处理器支持的注解,可用 *
指定全部注解 @SupportedSourceVersion
指定支持的java的版本
在注解处理器的 init 函数里,经过 Trees.instance(env)
拿到抽象语法树(AST)。 此处把ProcessingEnvironment
强转成JavacProcessingEnvironment
,后面的操做都变成了IDE编辑器内部的操做了。
在注解处理器的 process 函数中,咱们扫描全部的类,实现一个自定义的 TreeTranslator。
为何自定义的 TreeTranslator 要复写 visitBlock?由于咱们的需求场景是扫描全部 log 语句,粒度为语句块。AST 支持咱们以不一样的粒度去访问,还有哪些粒度呢?咱们看下TreeTranslator 的继承层次,能够发现一个 Visitor 类。
打开 Visitor 类:
全部 visit 方法一目了然,咱们前面提到 AST 每个节点都表明着源语言中的一个语法结构,因此咱们能够细粒度到指定访问 if、return、try等特定类型节点,只需覆写相应的 visit 方法。
回到咱们的需求场景:扫描全部 log 语句,既然是语句,粒度应该为语句块,因此咱们覆写 visitBlock 进行扫描,当扫描到指定语句好比 Log.
时,就不把整个语句都写入 AST,以此达到清除 log 语句的效果。
- 想了解更多 AST 操做语法?详见 java注解处理器——在编译期修改语法树
- 想获取 demo 源码请戳
有了实战的基础,咱们再来看看 ButterKnife 是如何利用 AST 的。全网对这块的讲解少之又少,解析只着重于 APT,实在惋惜。
细心的你会发如今 ButterKnife 的 sample-library 中,注解的都是引用了 R2 :
为何 library 工程不直接引用 R?当咱们把 R2 改为 R 以后,编译器会报错:
也就是说注解的属性必须是常量,可是 library 中 R.id.title 的值为变量。缘由见 Non-constant Fields in Case Labels.、Android主项目和Module中R类的区别。
那咱们能够拷贝下 R 文件,生成一个 R2,把属性都改成常量便可解决。为了让这个拷贝过程无感知,J 神使用了 gradle 插件来自动化完成,这就是 library 须要引用 butterknife-gradle-plugin 的缘由。
那另外一个问题来了,R2 仅仅是 module 中 R 的复制,只表明了所在 module 编译期间 R 的值,在运行时主工程的 R 和 R2 彻底对不上,单纯地拷贝修改是不行的。咋整呢?
那咱们生成 R2 供编译期使用,在生成代码阶段把 R2 替换成 R 不就好了?好主意!J 神的思路就是这样的!咱们打开生成的 XXX_ViewBinding
文件就能够发现 —— R2 已经被换成了 R。
可是怎么拿到 R 和 R2 的映射呢?
咱们思考下:以 @BindView(R2.id.view)
为例,最终生成的代码是 findViewById(0x7f…)
。那咱们经过 0x7f…
反寻 R2.id.view
这样的常量名,R 和 R2 同样,因此也连带知道了 R.id.view
变量名,因而能够将生成代码的结果从 findViewById(0x7f…)
替换成 findViewById(R.id.view)
,这里的 R
在主工程的编译过程当中会被 inline 成最终肯定的数值,从而避免在生成代码的过程当中直接填写数值带来的麻烦。
思路肯定了,那接下来第一步就是经过 0x7f…
反寻 R2.id.view
,可是在 APT 里,咱们只能拿到 Element 的注解值,也就是说,并不知道当前传入的是 R2 的哪一个 field。如今就该轮到 AST 大显身手了,根据 Element 反查出真正 Java 文件的树形结构。
你觉得 AST 的应用场景就这么多了吗?
不不不,咱们开下脑洞,既然拿到了源代码的树形表达式,咱们不必定要把表达式转回成源码,那是否是能够经过它自动写代码?画个源码流程图?画个类图?写个说明文档?或者其它你想要的东西?
看看这个项目 js-code-to-svg-flowchart,或许能给带你更多灵感。
也许下面这些资料能够答疑:
本篇完成耗时 24 个番茄钟(600 分钟)
我是 FeelsChaotic,一个写得了代码 p 得了图,剪得了视频画得了画的程序媛,致力于追求代码优雅、架构设计和 T 型成长。
欢迎关注 FeelsChaotic 的简书和掘金,若是个人文章对你哪怕有一点点帮助,欢迎 ❤️!你的鼓励是我写做的最大动力!
最最重要的,请给出你的建议或意见,有错误请多多指正!