注解有时候也称为「元编程」,它是对代码本身进行编码,让程序自动生成代码。这种理解有一点狭义,注解不一点要生成代码,也可能只是给其他代码或 IDE 作为参考。例如 @Deprecated 就是很好的例子。

在 java 中,定义注解语法如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAnnotation {
  int value();
  String name() default "unknown";
}

一般情况下应该给注解加上两个维度的限制:

  • @Retention: 限制注解的有效时间。SOURCE 是只在编译过程中有效,不会出现在字节码中。CLASS 会把注解编译到字节码中,但不会载入 runtime。RUNTIME 顾名思义,在运行时也可以读取。
  • @Target: 指明注解的作用域。例如限制为只能用于字段,或只能用于方法等。

定义注解的关键字是 @interface,所以注解内部的写法也和接口有一点类似。不同的是,注解内部的方法会被解析为字段与参数使用。上面的代码使用起来是这样的:

@MyAnnotation(20, name="Chenhe")
Object obj;

value() 是一个特殊的方法,在使用是不需要传递参数名,而其他方法都必须写上名字来赋值。

注解只定义没有用,还需要去处理。处理注解的代码就是普通的 java 代码了。

手写 butterknife

butterknife 是一个用于 Android 的注解工具,主要作用是简化 findViewById() 操作。基本用法如下:

@BindView(R.id.textView) TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  Binding.bind(this);
}
butterknife 不算是依赖注入。View 的值依然是 Activity 内部处理的,butterknife 只是简化这个过程。严格意义上的依赖注入,值是外部提供的。例如构造参数可以视为一种手动的依赖注入。

我们就手写一下这个功能来熟悉注解处理的使用。

先定义出我们的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

下面开始写具体处理逻辑。

基本反射实现

用反射处理注解是最简单的一种。

写法非常粗暴:取得 Activity 的所有字段,筛选出带有 BindView 注解的那些,调用 Android API 取得 View 赋值给它们。

public class Binding {
    public static void bind(Activity activity) {
        for (Field field : activity.getClass().getDeclaredFields()) {
            BindView bv = field.getAnnotation(BindView.class);
            if (bv != null) {
                field.setAccessible(true);
                try {
                    field.set(activity, activity.findViewById(bv.value()));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

可惜反射存在一定的性能问题,至少,从感觉上来说存在性能问题。其实就一般用例而言,我们这种写法问题不大。但既然这是一个 lib,就得做好面对各种使用场合的准备,如果恰好某个 Activity View 比较多的话,这么反射就不合适了。

Annotation Processor

宏观原理

上面基本的反射实现问题在于如果 View 变多,就需要多次反射,影响速度。现在我们打算给每一个 Activity 创建一个对应的 ViewBinding 类,在这个类中以代码的形式(而不是反射)写上所有的 View 与 findViewById() 方法。在真正的 Activity 中实例化这个帮助类就能完成绑定。

例如针对 MainActivity,创建帮助类 MainActivityViewBinding 如下:

// MainActivityViewBinding.java
public class MainActivityViewBinding {
  public MainActivityViewBinding(MainActivity activity) {
    activity.textView = activity.findViewById(2131230959);
    activity.layout = activity.findViewById(2131230864);
  }
}

那么在 MainActivity 中只需要这么写:

// MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  new MainActivityViewBinding(this); // 完成绑定
}

Annotation Processor 内要做的事情就是自动生成这些 ViewBinding 类,也就是一开始所说的「代码生成」。

项目结构

首先需要一个 Android Lib Module 编写 Binding 入口方法。

还需要一个 Java Lib Module 编写 Annotation Processor。

上面的两个模块都需要用到注解定义本身。注意依赖中的两个限制:

  • Java module 不能依赖 Android module。
  • annotationProcessor 依赖只能执行注解处理器,不能依赖普通代码。

为了便于使用,开发就相对麻烦一点,这里创建三个模块:

  • Java Lib lib-annotations 存放注解定义。
  • Java Lib lib-processor 依赖 lib-annotations,编写注解处理器。
    注意注解处理器仅支持打包成 jar 不支持 aar,所以必须是 java moudle。
  • Android Lib lib 传递依赖 lib-annotations,编写入口函数。

最终使用时,添加两个依赖就好了:

implementation project(':lib')
annotationProcessor project(':lib-processor')

注解定义和基本反射实现一样的,不赘述了。

代码生成

编写注解处理器有两步:

  1. 继承 AbstractProcessor 类实现注解处理器。
  2. 添加资源文件描述,使其他项目能识别我们的处理器。
    这一步写法固定,不需要记住,需要的时候翻一翻笔记就行了。具体来讲就是创建一个文件:./src/main/resources/META-INF/services/javax.annotation.processing.Processor,里面直接写上实现的类。

注解处理器一般至少需要覆盖三个方法:

  • init(): 初始化方法。例如取得 Filer
  • getSupportedAnnotationTypes(): 返回要处理的注解的名称。
  • process() 处理注解。
也可以给处理器添加 @SupportedAnnotationTypes 注解来指明要处理的注解类型,但这种方法不能通过 class 自动获取了。所以我们选择覆盖 getSupportedAnnotationTypes() 方法。

代码生成方法有很多,最粗暴的就是自己构建字符串,再写到 xxx.java 文件,显然这不是个好办法。java 原生提供了 Filer 类帮助找到合适的文件存放位置。配合 square 的开源库 javapoet 进一步简化代码生成,记得添加依赖哦。

process() 方法签名如下:

boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

set 参数是本轮处理所包含的请求处理的注解类型。注意,是注解类型,不是注解实例。这意味着多次使用注解这个集合里只会有一个。 这个参数不是很常用。

roundEnvironment 是本轮处理的所有上下文数据。

处理注解的过程中生成的代码可能包含注解,所以注解处理有多轮。

要生成代码,需要以下数据:

  • 使用注解所在的包名。
  • 使用注解所在的类名。
  • 使用注解的变量和注解实例。

roundEnvironment.getRootElements() 可拿到所有根 Element。这里 Element 代表的是代码层面的元素,比如包、类、接口、方法、字段等。所谓根,就是本轮所有用到注解的地方会按照位置聚类,返回顶层元素。在这个用例中应该是各个 Activity Class(此处「顶层」不会返回包级别)。

拿到 Activity 的 Element,它的父级就是 Package。子集就包含各个字段。整体实现如下:

public class BindingProcessor extends AbstractProcessor {
  private Filer filer;

  @Override
  public Set<String> getSupportedAnnotationTypes() {
    return Collections.singleton(BindView.class.getCanonicalName());
  }

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    filer = processingEnv.getFiler();
  }

  @Override
  public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    for (Element e : roundEnvironment.getRootElements()) {
      String pkgName = e.getEnclosingElement().toString(); // class 的父级是 package
      String clsName = e.getSimpleName().toString();
      ClassName genCls = ClassName.get(pkgName, clsName + "ViewBinding"); // 要生成的类名
      MethodSpec.Builder builder = MethodSpec.constructorBuilder() // 创建一个构造方法
        .addModifiers(Modifier.PUBLIC)
        .addParameter(ClassName.get(pkgName, clsName), "activity"); // 参数就是当前 Activity
      boolean hasBinding = false;

      for (Element child : e.getEnclosedElements()) {
        if (child.getKind() != ElementKind.FIELD) // 只处理字段元素
          continue;
        BindView anno = child.getAnnotation(BindView.class);
        if (anno == null)
          continue;
        hasBinding = true;
        builder.addStatement("activity.$N = activity.findViewById($L)", child.getSimpleName(), anno.value()); // 生成 findViewById 方法调用
      }
      if (hasBinding) {
        TypeSpec builtClass = TypeSpec.classBuilder(genCls) // 要生成的类
          .addModifiers(Modifier.PUBLIC)
          .addMethod(builder.build())
          .build();
        try {
          JavaFile.builder(pkgName, builtClass)
            .build().writeTo(filer); // 写入文件
        } catch (IOException ex) {
          ex.printStackTrace();
        }
      }
    }
    return true; // 不再允许其他注解处理器z
  }
}
这个实现仅作为演示,并不完善。例如把 @BindView 写在奇奇怪怪的地方可能会出问题。

在 App 项目中引用我们的注解处理器项目,构建一下(只 sync 是不行的),就能找到生成的文件:

// ./app/build/generated/ap_generated_sources/debug/out/.../MainActivityViewBinding.java
// 这些代码是自动生成的
public class MainActivityViewBinding {
  public MainActivityViewBinding(MainActivity activity) {
    activity.textView = activity.findViewById(2131230959);
    activity.layout = activity.findViewById(2131230864);
  }
}

调用帮助类

帮助类已经自动生成了,下面要调用它才行。每一个 Activity 都有自己的帮助类,为了正确调用依然得使用反射。但这么用几乎不会影响性能,因为只在 Activity 创建时反射一次,无论内部有多少个 View 都无需反射,这点耗时是可以接受的。

public class Binding {
  public static void bind(Activity activity) {
    try {
      // 利用反射找到生成的类
      Class<?> bindingClass = Class.forName(activity.getClass().getCanonicalName() + "ViewBinding");
      // 找到构造函数来实例化
      Constructor<?> constructor = bindingClass.getDeclaredConstructor(activity.getClass());
      constructor.newInstance(activity);
    } catch (ClassNotFoundException | InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
      e.printStackTrace();
    }
  }
}

这个 Demo 到此就完成了,和 butterknife 使用方法一致。

Last modification:November 13, 2022