1. 程式人生 > >ButterKnife 是怎麼解決 library 的 R 問題的

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";
  }
}

這樣完全不需要任何耗時耗力的反查工作,直接拿來用就好了。