看啥推荐读物
专栏名称: XingJimmy
目录
相关文章推荐
今天看啥  ›  专栏  ›  XingJimmy

ButterKnife源码拆轮子学习2-AST学习

XingJimmy  · 掘金  ·  · 2019-08-20 00:42
阅读 12

ButterKnife源码拆轮子学习2-AST学习

上篇文章 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 成最终确定的数值,从而避免在生成代码的过程中直接填写数值带来的麻烦。

参考 juejin.im/post/5c45bc… www.jianshu.com/p/5514cf705…




原文地址:访问原文地址
快照地址: 访问文章快照