写点什么

AOP 最后一块拼图 _ AST 抽象语法树 —— 最轻量级的 AOP 方法

用户头像
Android架构
关注
发布于: 1 小时前

![转化图解](https://user-gold-cdn.xitu.io/2019/1/21/1687069b6bec4b42?imageView2/0/w/1280/h/960/ignore-error/1


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


)


怎么利用 AST?

我们可以发现,AST 定义了代码的结构,通过操作 AST,我们可以精准地定位到声明语句、赋值语句、运算语句等,实现对源代码的分析、优化、变更等操作。


举个例子,想要改变 a 的赋值,如下图:



想改 a 的赋值,可以对 AST 语法树的 value 节点下手,一旦改动,编译器会重新进行编译流程处理,此时赋值改动就反映到源码上了。是不是很神奇?其实 Lombok、IDE 语法高亮、IDE 格式化代码、自动补全、代码混淆压缩、甚至大名鼎鼎的 ButterKnife 的 R、R2 文件映射和静态代码检查,都是利用了 AST。


既然要操作 AST,我们怎么拿到 AST 呢?


答案是:在注解处理器 APT!


利用 JDK 的注解处理器,可在编译期间处理注解,还可以读取、修改、添加 AST 中的任意元素,让改动后的 AST 重新参与编译流程处理,直到语法树没有改动为止。


AST 优缺点

相比其他的 AOP 方法,AST 属于编辑器级别,时机更为提前,效率更高。



但语法复杂,推荐通过库来操作 AST:


二、实践

实现一个清除 log 功能

整体思路:在编译期间拿到 AST,扫描是否含有特定日志语句如:Log,存在则删除该语句。

1. 实现 AbstractProcessor

2. 添加注解

@SupportedAnnotationTypes 指定此注解处理器支持的注解,可用 * 指定所有注解 @SupportedSourceVersion 指定支持的 java 的版本


3. 获取 AST


在注解处理器的 init 函数里,通过 Trees.instance(env) 拿到抽象语法树(AST)。 此处把ProcessingEnvironment强转成JavacProcessingEnvironment,后面的操作都变成了 IDE 编辑器内部的操作了。

4. 操作 AST


在注解处理器的 process 函数中,我们扫描所有的类,实现一个自定义的 TreeTranslator。



为什么自定义的 TreeTranslator 要复写 visitBlock?因为我们的需求场景是扫描所有 log 语句,粒度为语句块。AST 支持我们以不同的粒度去访问,还有哪些粒度呢?我们看下 TreeTranslator 的继承层次,可以发现一个 Visitor 类。



打开 Visitor 类:



所有 visit 方法一目了然,我们前面提到 AST 每一个节点都代表着源语言中的一个语法结构,所以我们可以细粒度到指定访问 if、return、try 等特定类型节点,只需覆写相应的 visit 方法。


回到我们的需求场景:扫描所有 log 语句,既然是语句,粒度应该为语句块,所以我们覆写 visitBlock 进行扫描,当扫描到指定语句比如 Log. 时,就不把整个语句都写入 AST,以此达到清除 log 语句的效果。



剖析 ButterKnife

有了实战的基础,我们再来看看 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 文件的树形结构。


  1. 拿到 AST 树;



  1. 在扫描资源时,获取 Element AST 树,注入自定义的 TreeScanner 访问器 RScanner 来访问子节点;



  1. RScanner 寻找 R 文件内部类(id、string 等)),建立 view 与 id 的关系;



  1. 拿到映射关系后,进行代码拼接。



扩展

AST 应用场景扩展

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
AOP 最后一块拼图 _ AST 抽象语法树 —— 最轻量级的AOP方法