ButterKnife 是怎么解决 library 的 R 问题的

问题

Annotation 中必须引用 final 的值(编译期已经有最终值),而 ButterKnife 中引用的 R (在 library 工程中)是非 final 的。

ButterKnife 的 tricks

生成 R2

既然 R 不是 final 的,生成一个呗。所以 butterknife 实现了一个 plugin,把 R 拷贝出了一个 final 版本: R2。这个非常直接有效,这个代码很短,只是一个 plugin 在每个配置上都加了一个拷贝 R 的 task。
这样就万事大吉了吗?完全不是,R2 只代表了在本 module 编译期间 R 的值,然而,在运行时 R 和 R2 完全对不上。

根据 R2 反查 R

看 ButterKnife 生成的代码,会发现里面并没有引用 R2,而是直接使用的 R。这就是为了解决运行期和编译期 R 值不一致的问题。怎么做到的呢?
在 apt 时,能得到的 Element、 annotation 都是返回值的。也就是说,并不知道当前传入的是 R2 的哪个 field。虽然 R2 和 R 在 module 编译期的值是一致的,也并不能直接找到。
这就衍生了用值构建 R 和 R2 对应关系的需求。这里需要用到一个新的 api:com.sun.source.util。这个库能够根据 Element 反查出真正 java 文件的树形结构,里面能够获取编译期对应的 AST 语言元素,比如调用、比较等等。这样就可以做赋值分析、代码进一步解构等工作。
ButterKnife 源码:


  private void scanForRClasses(RoundEnvironment env) {
    if (trees == null) return;

    RClassScanner scanner = new RClassScanner();
    // 所有 annotation 里的值 都是来自于 R2 的
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
      for (Element element : env.getElementsAnnotatedWith(annotation)) {
        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
          scanner.setCurrentPackage(elementUtils.getPackageOf(element));
          tree.accept(scanner);
        }
      }
    }

    for (Map.Entry<PackageElement, Set<Symbol.ClassSymbol>> packageNameToRClassSet
          : scanner.getRClasses().entrySet()) {
      PackageElement respectivePackageName = packageNameToRClassSet.getKey();
      for (Symbol.ClassSymbol rClass : packageNameToRClassSet.getValue()) {
        parseRClass(respectivePackageName, rClass, scanner.getReferenced());
      }
    }
  }

  private void parseRClass(PackageElement respectivePackageName, Symbol.ClassSymbol rClass,
      Set<String> referenced) {
    TypeElement element;
    // 各种奇技淫巧才能work
    try {
      element = rClass;
    } catch (MirroredTypeException mte) {
      element = (TypeElement) typeUtils.asElement(mte.getTypeMirror());
    }

    JCTree tree = (JCTree) trees.getTree(element);
    if (tree != null) { // tree can be null if the references are compiled types and not source
      IdScanner idScanner =
          new IdScanner(symbols, elementUtils.getPackageOf(element), respectivePackageName,
              referenced);
      tree.accept(idScanner);
    } else {
      parseCompiledR(respectivePackageName, element, referenced);
    }
  }

// 针对 final 的
  private void parseCompiledR(PackageElement respectivePackageName, TypeElement rClass,
      Set<String> referenced) {
    for (Element element : rClass.getEnclosedElements()) {
      String innerClassName = element.getSimpleName().toString();
      if (SUPPORTED_TYPES.contains(innerClassName)) {
        for (Element enclosedElement : element.getEnclosedElements()) {
          if (enclosedElement instanceof VariableElement) {
            String fqName = elementUtils.getPackageOf(enclosedElement).getQualifiedName().toString()
                + ".R."
                + innerClassName
                + "."
                + enclosedElement.toString();
            if (referenced.contains(fqName)) {
              VariableElement variableElement = (VariableElement) enclosedElement;
              Object value = variableElement.getConstantValue();

              if (value instanceof Integer) {
                int id = (Integer) value;
                ClassName rClassName =
                    ClassName.get(elementUtils.getPackageOf(variableElement).toString(), "R",
                        innerClassName);
                String resourceName = variableElement.getSimpleName().toString();
                QualifiedId qualifiedId = new QualifiedId(respectivePackageName, id);
                symbols.put(qualifiedId, new Id(id, rClassName, resourceName));
              }
            }
          }
        }
      }
    }
  }

  private static class RClassScanner extends TreeScanner {
    // Maps the currently evaluated rPackageName to R Classes
    private final Map<PackageElement, Set<Symbol.ClassSymbol>> rClasses = new LinkedHashMap<>();
    private PackageElement currentPackage;
    private Set<String> referenced = new HashSet<>();

    @Override public void visitSelect(JCTree.JCFieldAccess jcFieldAccess) {
      Symbol symbol = jcFieldAccess.sym;
      // 这个是各种层次,具体不清楚 只能抄
      if (symbol != null
          && symbol.getEnclosingElement() != null
          && symbol.getEnclosingElement().getEnclosingElement() != null
          && symbol.getEnclosingElement().getEnclosingElement().enclClass() != null) {
        Set<Symbol.ClassSymbol> rClassSet = rClasses.get(currentPackage);
        if (rClassSet == null) {
          rClassSet = new HashSet<>();
          rClasses.put(currentPackage, rClassSet);
        }
        referenced.add(getFqName(symbol));
        rClassSet.add(symbol.getEnclosingElement().getEnclosingElement().enclClass());
      }
    }

    Map<PackageElement, Set<Symbol.ClassSymbol>> getRClasses() {
      return rClasses;
    }

    Set<String> getReferenced() {
      return referenced;
    }

    void setCurrentPackage(PackageElement packageElement) {
      this.currentPackage = packageElement;
    }
  }

  private static class IdScanner extends TreeScanner {
    private final Map<QualifiedId, Id> ids;
    private final PackageElement rPackageName;
    private final PackageElement respectivePackageName;
    private final Set<String> referenced;

    IdScanner(Map<QualifiedId, Id> ids, PackageElement rPackageName,
        PackageElement respectivePackageName, Set<String> referenced) {
      this.ids = ids;
      this.rPackageName = rPackageName;
      this.respectivePackageName = respectivePackageName;
      this.referenced = referenced;
    }

    @Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
      for (JCTree tree : jcClassDecl.defs) {
        if (tree instanceof ClassTree) {
          ClassTree classTree = (ClassTree) tree;
          String className = classTree.getSimpleName().toString();
          if (SUPPORTED_TYPES.contains(className)) {
            ClassName rClassName = ClassName.get(rPackageName.getQualifiedName().toString(), "R",
                    className);
            VarScanner scanner = new VarScanner(ids, rClassName, respectivePackageName, referenced);
            ((JCTree) classTree).accept(scanner);
          }
        }
      }
    }
  }

  private static class VarScanner extends TreeScanner {
    private final Map<QualifiedId, Id> ids;
    private final ClassName className;
    private final PackageElement respectivePackageName;
    private final Set<String> referenced;

    private VarScanner(Map<QualifiedId, Id> ids, ClassName className,
        PackageElement respectivePackageName, Set<String> referenced) {
      this.ids = ids;
      this.className = className;
      this.respectivePackageName = respectivePackageName;
      this.referenced = referenced;
    }

    @Override public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl) {
      if ("int".equals(jcVariableDecl.getType().toString())) {
        String resourceName = jcVariableDecl.getName().toString();
        if (referenced.contains(getFqName(jcVariableDecl.sym))) {
          int id = Integer.valueOf(jcVariableDecl.getInitializer().toString());
          QualifiedId qualifiedId = new QualifiedId(respectivePackageName, id);
          ids.put(qualifiedId, new Id(id, className, resourceName));
        }
      }
    }
  }

我抄过来用的要简单很多:

public void onAnnotatedElement(Element field, Class<? extends Annotation> annotation) {
    JCTree tree = (JCTree) mTrees.getTree(field, getMirror(field, annotation));
    tree.accept(new TreeScanner() {
      @Override
      public void visitSelect(JCTree.JCFieldAccess jcFieldAccess) {
        Symbol symbol = jcFieldAccess.sym;
        if (symbol != null
            && symbol.getEnclosingElement() != null
            && symbol.getEnclosingElement().getEnclosingElement() != null
            && symbol.getEnclosingElement().getEnclosingElement().enclClass() != null) {
          TypeElement rClass;
          try {
            rClass = symbol.getEnclosingElement().getEnclosingElement().enclClass();
          } catch (MirroredTypeException mte) {
            rClass = (TypeElement) mTypes.asElement(mte.getTypeMirror());
          }
          parseR2(rClass);
        }
      }
    });
  }

  private void parseR2(Element r2Class) {
    TypeElement r2Idclass = null;
    for (Element idClass : r2Class.getEnclosedElements()) {
      if (idClass.getKind() != ElementKind.CLASS || !idClass.toString().endsWith(".id")) {
        continue;
      }
      r2Idclass = (TypeElement) idClass;
      break;
    }
    if (r2Idclass == null) {
      return;
    }

    if (!mParsedR.add((TypeElement) r2Class)) {
      return;
    }

    TypeElement rClass = mElements.getTypeElement(mElements.getPackageOf(r2Class) + ".R.id");

    for (Element idField : r2Idclass.getEnclosedElements()) {
      if (idField.getKind() != ElementKind.FIELD) {
        continue;
      }
      Optional.ofNullable(AptUtils.assignedValue(idField, mTrees))
          .map(val -> Integer.parseInt(val))
          .map(id -> {
            return mId2NameMapping.put(id, new RClassField(idField.toString(), rClass));
          });
    }
  }

  public static String assignedValue(Element field, Trees trees) {
    Tree tree = trees.getTree(field);
    if (tree == null) {
      return String.valueOf(((VariableElement) field).getConstantValue());
    } else {
      ExpressionTree expression = ((VariableTree) tree).getInitializer();
      return Optional.ofNullable(expression)
          .filter(e -> e instanceof LiteralTree)
          .map(e -> e.toString()).orElse(null);
    }
  }

我的方案

其实 R2 不需要是 R 的翻版,而可以是 String 类型的、R 的对应关系。例如:

R2{
  id{
    final String some_id_in_r = "xxx.R.id.some_id_in_r";
  }
}

这样完全不需要任何耗时耗力的反查工作,直接拿来用就好了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值