Android编译时注解初级之ButterKnife

本文的主要目的在于了解编译时注解,并能初步运用。代码在最后。

1.编译时注解 VS 运行时注解

1.1 运行时注解

这种注解在运行时,依赖反射,获得需要的信息,比如:

    @SyntaxRun(R.id.id_text)
    public Button mText;

    ...

    private void processAnnotations() {
        Field[] fields = this.getClass().getDeclaredFields();
        for (Field f : fields) {
            SyntaxRun sr = f.getAnnotation(SyntaxRun.class);
            if (sr == null) {
                continue;
            }

            int id = sr.value();
            if (id == -1) {
                continue;
            }

            try {
                f.set(this, findViewById(id));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

这样,就可以实现自动findview了。但是这样会在初始化的时侯,依赖反射去找到,并且设置,这一过程又会导致性能上的损失。

1.2 编译时注解

顾名思义,这个注解是在编译的时侯生效,那么唯一的做法就是帮我们生成那些findview的类。先看一个例子,后面再看完成代码,比如:

    private void processGen(RoundEnvironment roundEnv) {
        Set elements =
                roundEnv.getElementsAnnotatedWith(SyntaxGen.class);

        String enclosingClass = null;
        List views = new ArrayList<>();

        for (Element each : elements) {

            Element enclosingElement = each.getEnclosingElement();
            JCTree tree = (JCTree) mTrees.getTree(enclosingElement);
            //tree.accept(new DeprecatedTranslator(roundEnv, each, mTreeMaker, mMethodHelper, mElementUtils));

            if (!(each instanceof Symbol.VarSymbol)) {
                continue;
            }

            Symbol.VarSymbol symbol = (Symbol.VarSymbol) each;
            String viewClass = symbol.asType().toString();
            System.out.println("each : " + viewClass);
            System.out.println("name : " + each.getSimpleName());
            System.out.println("tree enclosing : " + enclosingElement);
            
            //1.拿到资源id
            int resId = each.getAnnotation(SyntaxGen.class).value();
            if (enclosingClass == null) {
                enclosingClass = enclosingElement.toString();
                System.out.println("class : " + enclosingClass);
            }

            views.add(new FieldDesc(viewClass, each.getSimpleName().toString(), resId));
        }
        //2. 生成代码
        if (enclosingClass != null) {
            generateClass(enclosingClass, views);
        }
    }

先粗略看一下编译时注解,后面会解释。要做的就是通过注解拿到resid,然后生成代码帮我们绑定。
与运行时注解相比,只是实现方式不同罢了。

2.ButterKnife

早期版本的BK也是运行时注解,也是后来改成了编译时注解。
现在分析一下这个库的实现,分析开源库首先抓它的核心,不用太在意细枝末节。
依我们的用途,无非就是不再频繁的写findview,setonclick等,那么BK的BindView注解就是我们最常用的,先看一下,它时如何实现的。

看ButterKnifeProcessor.java这个类,以BindView为例,其他都类似的。
1.查找注解修饰的对象

从process方法开始分析

@Override public boolean process(Set elements, RoundEnvironment env) {
    //1.获取注解的信息等
    Map bindingMap = findAndParseTargets(env);

    for (Map.Entry entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
      //2.生成java类文件
      JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }

两步走:
一,找注解拿信息
二,生成java类文件。

private Map findAndParseTargets(RoundEnvironment env) {
  ...
    // Process each @BindView element.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      // we don't SuperficialValidation.validateElement(element)
      // so that an unresolved View type can be generated by later processing rounds
      try {
        System.out.println("BindView Element : " + element);
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
  ...
}

2.解析找到的element,element是编译时的对象,比如一个变量,一个类等。
主要看核心的点。


  private void parseBindView(Element element, Map builderMap,
      Set erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Start by verifying common generated code restrictions.
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);

    // Verify that the target type extends from View.
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();

    ...错误判断
    // Assemble information on the field.
    //拿到id
    int id = element.getAnnotation(BindView.class).value();

    //1.得到一个Builder
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    Id resourceId = elementToId(element, BindView.class, id);
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(resourceId);
      if (existingBindingName != null) {
        error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
            BindView.class.getSimpleName(), id, existingBindingName,
            enclosingElement.getQualifiedName(), element.getSimpleName());
        return;
      }
    } else {
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }
    
    //2.拿到名字
    String name = simpleName.toString();
    //拿到变量的类型
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);
    
    //3.添加到Builder
    builder.addField(resourceId, new FieldViewBinding(name, type, required));

    // Add the type-erased version to the valid binding targets set.
    erasedTargetNames.add(enclosingElement);
  }

2.1 获取一个Builder,没有则创建。
参考如下创建builder,可以看出,平时用BK时,编译过后生成的类就是在这准备的。
比如:
ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

如果在MainActivity中使用,那么就有 MainActivity_ViewBinding.java


  static Builder newBuilder(TypeElement enclosingElement) {
    TypeMirror typeMirror = enclosingElement.asType();

    boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
    boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
    boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);

    TypeName targetType = TypeName.get(typeMirror);
    if (targetType instanceof ParameterizedTypeName) {
      targetType = ((ParameterizedTypeName) targetType).rawType;
    }

    String packageName = getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(
        packageName.length() + 1).replace('.', '$');
    //这里是不是熟悉了?
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
  }

2.2 拿到名字和类型等信息。
2.3 添加到Builder中。

这样,我们的信息就准备好了。然后在ButterKnifeProcessor的process中看到
JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);
会生成一个文件,写入到编译路径里面,那么写了什么呢?
按之前我们分析的BindView的路线,继续往下看。
最终,我们看到会进入如下的方法里面:
这里就比较熟悉了,最终生成了 target.view = source.findViewById(id)这样的语句。
!注意,用的是target.view这种直接调用的方式,这也是变量必须不能是private的原因。

现在这里出现了一些$L $T 等这些东西,是什么呢?这里用了生成java文件的一个库:JavaPoet , github自行搜索,后面也会用到。


   private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
    if (binding.isSingleFieldBinding()) {
      // Optimize the common case where there's a single binding directly to a field.
      FieldViewBinding fieldBinding = binding.getFieldBinding();
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());

      boolean requiresCast = requiresCast(fieldBinding.getType());
      if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
        if (requiresCast) {
          builder.add("($T) ", fieldBinding.getType());
        }
        builder.add("source.findViewById($L)", binding.getId().code);
      } else {
        builder.add("$T.find", UTILS);
        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
        if (requiresCast) {
          builder.add("AsType");
        }
        builder.add("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
          builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        }
        if (requiresCast) {
          builder.add(", $T.class", fieldBinding.getRawType());
        }
        builder.add(")");
      }
      result.addStatement("$L", builder.build());
      return;
    }

    List requiredBindings = binding.getRequiredBindings();
    if (!debuggable || requiredBindings.isEmpty()) {
      result.addStatement("view = source.findViewById($L)", binding.getId().code);
    } else if (!binding.isBoundToRoot()) {
      result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,
          binding.getId().code, asHumanDescription(requiredBindings));
    }

    addFieldBinding(result, binding, debuggable);
    addMethodBindings(result, binding, debuggable);
  }

3.实现简单版的ButterKnife

3.1 呃呃呃,先去学习一下JavaPoet怎么用......
3.2 创建编译时注解(完整代码见后面)

1.创建类

public class SyntaxProcessor extends AbstractProcessor {
...
}

2.添加到路径
新建文件 resources/META-INF/services/javax.annotation.processing.Processor
并将SyntaxProcessor全路径名写在里面。

3.3 处理生成代码(按JavaPoet的形式生成)
private void generateClass(String originClass, List views) {

        String pkg = originClass.substring(0, originClass.lastIndexOf("."));
        String originClassName = originClass.substring(originClass.lastIndexOf(".") + 1);
        String newClass = originClassName + "_Bind";

        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(void.class)
                .addParameter(String[].class, "args")
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                .build();

        ClassName activityClass = ClassName.get(pkg, originClassName);
        FieldSpec activity = FieldSpec.builder(activityClass, "mA", Modifier.PRIVATE)
                .addModifiers(Modifier.PRIVATE, Modifier.PRIVATE)
                .build();

        List specs = new ArrayList<>();
        List sts = new ArrayList<>();
        for (FieldDesc desc : views) {
            String val = desc.viewClass;
            String pre = getPre(val);
            String suffix = getSuffix(val);

            ClassName viewClass = ClassName.get(pre, suffix);
            FieldSpec viewDef = FieldSpec.builder(viewClass, "mView_" + desc.id, Modifier.PUBLIC)
                    //.addModifiers(Modifier.PRIVATE, Modifier.PRIVATE)
                    .build();
            specs.add(viewDef);

            String st0 = "$N = $N.$N = this.$N.findViewById($L)";
            String stPre = "mView_" + desc.id;
            String st1 = "mA";
            String st2 = desc.fieldName;
            String st3 = "mA";
            int st4 = desc.id;
            sts.add(new IdSt(st0, stPre, st1, st2, st3, st4));
        }

        MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(activityClass, "activity")
                .addStatement("this.$N = $N", "mA", "activity");

        for (IdSt st : sts) {
            constructor.addStatement(st.st0, st.stPre, st.st1, st.st2, st.st3, st.st4);
        }

        TypeSpec.Builder builder = TypeSpec.classBuilder(newClass)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(constructor.build())
                .addMethod(main)
                .addField(activity);

        for (FieldSpec spec : specs) {
            builder.addField(spec);
        }

        JavaFile javaFile = JavaFile.builder(pkg, builder.build())
                .build();

        try {
            javaFile.writeTo(System.out);
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

生成后的如下:


package com.syntax.javapoet;

import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import java.lang.String;
import java.lang.System;

public final class MainActivity_Bind {
  private MainActivity mA;

  public Button mView_2131165252;

  public TextView mView_2131165251;

  public ImageView mView_2131165250;

  public MainActivity_Bind(MainActivity activity) {
    this.mA = activity;
    mView_2131165252 = mA.mText = this.mA.findViewById(2131165252);
    mView_2131165251 = mA.mTextView = this.mA.findViewById(2131165251);
    mView_2131165250 = mA.mImage = this.mA.findViewById(2131165250);
  }

  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

基本功能已经可以实现,想实现其他功能,再生成即可。

因一些原因,无法给出代码仓库,看这个吧。
代码见 https://pan.baidu.com/s/1oEjXjDiYehM-eIpg1D_TfQ

你可能感兴趣的:(Android编译时注解初级之ButterKnife)