上篇文章 juejin.im/post/5cdd2b… 对ButterKnife中如何使用APT来生成代码,做了简单的描述。
最近学习ButterKnife源码,发现其在APT中,还使用了AST技术。
AST介绍
AST(Abstract syntax tree)即为“抽象语法树”,是编辑器对代码的第一步加工之后的结果,是源代码的抽象语法结构的树状表现形式,源代码的每个元素映射到一个节点或子树。
AST生成过程
不同的语言,都会有对应不同的语法分析器,语法分析器会把源代码作为字符串读入、解析,并建立语法树,这是一个程序完成编译所必要的前期工作。
Java 的编译过程
词法分析,将源代码的字符流转变为 Token 列表。
一个个读取源代码,按照预定规则合并成 Token,Token 是编译过程的最小元素,关键字、变量名、字面量、运算符等都可以成为 Token。
语法分析,根据 Token 流来构造树形表达式也就是 AST。
语法树的每一个节点都代表着程序代码中的一个语法结构,如类型、修饰符、运算符等。经过这个步骤后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。
调用注解处理器。
如果注解处理器产生了新的源文件,新文件也要进行编译。
最后
语法树会被分析并转化成类文件。
AST优缺点
相比其他的AOP方法,AST 属于编辑器级别,时机更为提前,效率更高。
APT的三个弱点:
1、预留入口不编译会报红,正常运行就可以
2、反射获得新的类效率又太差
3、无法实现定点插桩,只能生成新的类
AST使用时机 注解处理器APT中
利用 JDK 的注解处理器,可在编译期间处理注解,还可以读取、修改、添加 AST 中的任意元素,让改动后的 AST 重新参与编译流程处理,直到语法树没有改动为止。
ButterKnife源码剖析
ButterKnife部分代码(AST相关)
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
// AST树
private Trees trees;
private RScanner rScanner;
@Override
public synchronized void init(ProcessingEnvironment env) {
try {
// 根据环境获取AST树
// Gets a Trees object for a given ProcessingEnvironment.
trees = Trees.instance(env);
rScanner = new RScanner(env.getMessager());
} catch (IllegalArgumentException ignored) {
}
}
/**
* 在扫描资源时,获取 Element AST 树,注入自定义的 TreeScanner 访问器 RScanner 来访问子节点;
* @param element
* @param annotation
* @param value
* @return
*/
private Id elementToId(Element element, Class<? extends Annotation> annotation, int value) {
// Gets the Tree node for a given Element.
// JCTree tree = (JCTree) trees.getTree(element);
// Gets the Tree node for an AnnotationMirror on a given Element.
// 比上面的方法更精准,获取注解的AST树
JCTree tree = (JCTree) trees.getTree(element, getMirror(element, annotation));
if (tree != null) {
// tree can be null if the references are compiled types and not source
rScanner.reset();
// 根据节点获取对应的AST树,开始访问子节点
tree.accept(rScanner);
if (!rScanner.resourceIds.isEmpty()) {
return rScanner.resourceIds.values().iterator().next();
}
}
return new Id(value);
}
/**
* TreeScanner A subclass of Tree.Visitor, this class defines a general tree scanner pattern.
* RScanner 寻找 R 文件内部类(id、string等)),建立 view 与 id 的关系;
*/
private static class RScanner extends TreeScanner {
Map<Integer, Id> resourceIds = new LinkedHashMap<>();
Messager msg;
RScanner(Messager msg) {
this.msg = msg;
}
/**
*
* @param jcFieldAccess Selects through packages and classes
*/
@Override public void visitSelect(JCTree.JCFieldAccess jcFieldAccess) {
msg.printMessage(Diagnostic.Kind.NOTE, "visitSelect");
// Root class for Java symbols.
// It contains subclasses for specific sorts of symbols, such as variables, methods and operators, types, packages.
// Each subclass is represented as a static inner class inside Symbol.
Symbol symbol = jcFieldAccess.sym;
if (symbol.getEnclosingElement() != null
&& symbol.getEnclosingElement().getEnclosingElement() != null
&& symbol.getEnclosingElement().getEnclosingElement().enclClass() != null) {
// enclClass() The closest enclosing class of this symbol's declaration.
// 判断是否有内部类,有的话取内部类的值
try {
// 具体的值
int value = (Integer) requireNonNull(((Symbol.VarSymbol) symbol).getConstantValue());
// 资源的名称比如android.R.color.primaryColor
Id id = new Id(value, symbol);
resourceIds.put(value, id);
msg.printMessage(Diagnostic.Kind.NOTE, String.format("visitSelect Id CodeBlock %s %l", id.code.toString(), value));
} catch (Exception ignored) { }
}
}
/**
* 建立View和ID的对应关系
* @param jcLiteral A constant value given literally.
*/
@Override public void visitLiteral(JCTree.JCLiteral jcLiteral) {
msg.printMessage(Diagnostic.Kind.NOTE, "visitLiteral");
try {
int value = (Integer) jcLiteral.value;
resourceIds.put(value, new Id(value));
} catch (Exception ignored) { }
}
void reset() {
resourceIds.clear();
}
}
}
public final class Id {
public Id(int value, Symbol rSymbol) {
this.value = value;
if (rSymbol != null) {
// The package which indirectly owns this symbol.
/**
* Returns a class name created from the given parts. For example, calling this with package name
* {@code "java.util"} and simple names {@code "Map"}, {@code "Entry"} yields {@link Map.Entry}.
* android R color colorPrimary
*/
ClassName className = ClassName.get(rSymbol.packge().getQualifiedName().toString(), R,
rSymbol.enclClass().name.toString());
String resourceName = rSymbol.name.toString();
this.code = className.topLevelClassName().equals(ANDROID_R)
? CodeBlock.of("$L.$N", className, resourceName)
: CodeBlock.of("$T.$N", className, resourceName);
this.qualifed = true;
// 生成的代码 ContextCompat.getColor(context, android.R.color.holo_green_dark);
} else {
this.code = CodeBlock.of("$L", value);
this.qualifed = false;
// 生成的代码 ContextCompat.getColor(context, 2131034155);
// 如果这种方式应用到资源上,那么一旦资源重新编译,值就会改变,而java代码没有改变的话,就会出现问题
}
}
}
复制代码
通过代码分析,ButterKnife在生成的文件中,使用的是资源的名称,而不是资源的值,为什么大神会这么复杂的设计呢?
public class BaseActivity_ViewBinding implements Unbinder {
@UiThread
public BaseActivity_ViewBinding(BaseActivity target) {
this(target, target);
}
/**
* @deprecated Use {@link #BaseActivity_ViewBinding(BaseActivity, Context)} for direct creation.
* Only present for runtime invocation through {@code ButterKnife.bind()}.
*/
@Deprecated
@UiThread
public BaseActivity_ViewBinding(BaseActivity target, View source) {
this(target, source.getContext());
}
@UiThread
public BaseActivity_ViewBinding(BaseActivity target, Context context) {
target.colorAccent = ContextCompat.getColor(context, R.color.colorAccent);
}
@Override
@CallSuper
public void unbind() {
}
}
复制代码
我们知道,Android在Library中生成的资源已经不在是final类型的,所以无法使用注解来处理。那我们可以拷贝下 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 成最终确定的数值,从而避免在生成代码的过程中直接填写数值带来的麻烦。