APT简介


Annotation Processing Tool ,即注解处理器。一般用来处理自定义的注解,然后根据注解生成一个辅助类。最著名的例子就是@BindView注解。

注意,这是在编译时扫描所以继承AbstractProcessor类,然后调用process方法去处理。因为是在编译的时候处理的,所以很多时候需要用到反射。

流程


  1. 创建一个java library库,用来提供注解
  2. 创建一个java library库,用来处理注解
  3. 在android app中引用

总体流程基本上分为这三个部分。但是有些时候,我们会在第2部到第3步之间创建一个android library库,然后在android app中直接引用这个android library

例子


初级

本例只看一下流程,对注解的处理比较粗糙。

  1. 创建java library库,命名为lib-anno,该库用来定义注解
    在该库中创建一个注解
// 当前注解所在包为 com.hhh.lib_anno
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int id();
}
  1. 再创建一个java library库,命名为lib-processor,该库用来处理注解
    首先在该module的build.gradle文件中引入依赖
apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // 引入注解的库
    implementation project(':lib-anno')

    // AutoService 注解
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
    implementation 'com.google.auto.service:auto-service-annotations:1.0-rc6'

    // 用于生成Java文件
    implementation 'com.squareup:javapoet:1.12.1'
}

sourceCompatibility = "8"
targetCompatibility = "8"

自定义Processor处理自定义的注解

// 所在包 com.hhh.lib_processor
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({
        "com.hhh.lib_anno.BindView"
})
public class MyProcessor extends AbstractProcessor {

    /**
     * @param set 需要处理的注解。也就是SupportedAnnotationTypes注解里面的内容
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                "开始处理注解了啦啦啦啦啦 ");

        // 获取所有被BindView注解的元素
        Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        if (set == null) {
            // 直接报错
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                    "没找到任何元素");
        }

        for (Element e : bindViewElements) {
            // 判断被BindView注解的元素是否是全局变量
            if (e.getKind() == ElementKind.FIELD) {
                VariableElement variableElement = (VariableElement) e;

                // 输出被注解的元素
                processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                        "variableElement : "+variableElement);

                BindView annotation = variableElement.getAnnotation(BindView.class);
                int id = annotation.id();
                // 输出注解的id
                processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                        "id : "+id);
            }
        }
        return true;
    }
}

当前process方法中仅仅是输出被BindView注解的元素以及对应的id,没有做其他处理

  • @AutoService
    google开源库,用来进行组件化开发的。如果没有该注解,可以自己手动注册processor
  • @SupportedSourceVersion
    提供支持的java版本号。如果没有该注解,需要重写getSupportedSourceVersion方法
  • SupportedAnnotationTypes
    提供需要处理的注解。如果没有该注解,需要重写getSupportedAnnotationTypes方法
  1. 在android app中使用
    在模块的build.gradle引入注解库,并配置注解处理库
// 在dependcies方法里面引入

// 引入注解库
implementation project(':lib-anno')
// apt 配置注解处理库
annotationProcessor project(':lib-processor')

在Activity中使用注解

public class MainActivity extends AppCompatActivity {

    //R.id.tv是在xml文件中的TextView的id
    @BindView(id = R.id.tv)
    public TextView mTextView;

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

好吧,三个步骤完成了,最后build一下,输出如下所示:

警告: 开始处理注解了啦啦啦啦啦 
警告: variableElement : mTextView
警告: id : 2131165359

可以看到,输出被注解的元素mTextView 以及id。

但是这个注解有什么用?好吧,没用,就是看个流程,接下来重新

进阶

接下来,将在AbstractProcessor处理BindView注解,将被BindView注解的View自动初始化,即做以下处理

mTextView = findViewById(R.id.tv);

但是要实现上面这一句咋办?生成一个辅助类呗,然后将对应的Activity传入进去,最终生成的类要类似这样的:

// 所在包 com.hhh.aptdemo
public class MainActivity$Helper implements IInjection {
  @Override
  public void inject(Object obj) {
    MainActivity a = (MainActivity) obj;
    a.mTextView = a.findViewById(2131165359);
  }
}

其中,IInjection 是自己定义的一个接口,方便对外提供。MainActivity名称是动态变化的,如果在其他Activity中,就需要换成其他Activity的名字。

  1. 在lib-anno中创建IInjection接口。要求所有被BindView注解元素所在类生成的辅助类都要继承它
package com.hhh.lib_anno;

public interface IInjection {
    void inject(Object activity);
}
  1. 重新写个Procesoor在app下使用

    public class MainActivity extends AppCompatActivity {
//R.id.tv是在xml文件中的TextView的id
@BindView(id = R.id.tv)
public TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    MainActivity$Helper helper = new MainActivity$Helper();
    helper.inject(this);

    mTextView.setText(" hello world ,this is test");
}

}

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({
        "com.hhh.lib_anno.BindView"
})
public class MyProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 获取所有被BindView注解的元素
        Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        if (set == null) {
            // 直接报错
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                    "没找到任何元素");
        }

        // 用来存储Activity以及Activity中被BindView注解的元素
        Map<TypeElement, Set<ViewInfo>> map = new HashMap<>();

        for (Element e : bindViewElements) {
            // 判断被BindView注解的元素是否是全局变量
            if (e.getKind() == ElementKind.FIELD) {

                // 被注解的元素
                VariableElement variableElement = (VariableElement) e;

                // 获取有BindView注解的元素的最直接外层
                // 例如在一个类A的全局变量B使用了注解,那么最直接外层元素就是类A
                Element enclosingElement = variableElement.getEnclosingElement();

                // 判断当前封装了BindView注解的最里层元素是否是类
                if (enclosingElement.getKind() == ElementKind.CLASS) {
                    TypeElement classEle = (TypeElement) enclosingElement;

                    // 填充集合,将BindView注解的View的名称与id一一对应起来,保存为ViewInfo
                    Set<ViewInfo> viewInfos = map.get(classEle);
                    if (viewInfos == null) {
                        viewInfos = new HashSet<>();
                        map.put(classEle, viewInfos);
                    }
                    BindView annotation = variableElement.getAnnotation(BindView.class);
                    ViewInfo info = new ViewInfo(variableElement.getSimpleName().toString(), annotation.id());
                    viewInfos.add(info);
                }
            }
        }

        createFile(map);
        return true;
    }

    private void createFile(Map<TypeElement, Set<ViewInfo>> map) {
        for (TypeElement typeElement : map.keySet()) {

            // 获取到类。即含有BindView注解元素的类。本例子中就是MainActivity
            Name simpleName = typeElement.getSimpleName();

            // 创建代码块
            CodeBlock.Builder builder = CodeBlock.builder()
                    // 强制转换为对应的类
                    .addStatement("$N a = ($N) obj", typeElement.getSimpleName(), typeElement.getSimpleName());
            for (ViewInfo info : map.get(typeElement)) {
                builder.addStatement("a.$L = a.findViewById($L)", info.viewName, info.id);
            }

            // 创建参数
            ParameterSpec objPara = ParameterSpec.builder(Object.class, "obj")
                    .build();

            // 创建方法
            MethodSpec injectMethod = MethodSpec.methodBuilder("inject")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addParameter(objPara)
                    .addCode(builder.build())
                    .build();

            // 创建类
            TypeSpec helperClass = TypeSpec.classBuilder(simpleName.toString() + "$Helper")
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(IInjection.class)
                    .addMethod(injectMethod)
                    .build();

            System.out.println(typeElement.getQualifiedName());


            // 获取当前类所在的包
            PackageElement packageEle = processingEnv.getElementUtils().getPackageOf(typeElement);

            // 创建java文件
            JavaFile javaFile = JavaFile.builder(packageEle.getQualifiedName().toString(), helperClass).build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 创建一个Android Library的module
    该module的作用就是为了想外提供api,隐藏生成的辅助类,不要app直接访问辅助类。
    首先,build.gradle文件中,需要引用 IInjection 所在的库
implementation project(':lib-anno')

再创建一个类,用来提供api

public class Registrar {

    private Registrar() {

    }

    private static class SingleHolder {
        public static final Registrar INSTANCE = new Registrar();
    }

    public static Registrar getInstance() {
        return SingleHolder.INSTANCE;
    }

    public void register(Activity activity) {
        // 通过反射获取Activity的辅助类,然后创建实例,在
        try {
            Class<?> clazz = Class.forName(activity.getClass().getName() + "$Helper");
            // 因为规定所有的Helper类都继承了IInjection,所以直接强制转换
            IInjection iInjection = (IInjection) clazz.newInstance();
            iInjection.inject(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里直接通过反射来获取生成的Helper类,在使用的时候根本不需要知道生成的Helper类的名称以及所在地址

  1. 在app中使用
    首先在build.gradle中引入common库
// 引入common库
implementation project(':lib-common')

然后再Activity中使用注解,并注册

public class MainActivity extends AppCompatActivity {

    //R.id.tv是在xml文件中的TextView的id
    @BindView(id = R.id.tv)
    public TextView mTextView;

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

        Registrar.getInstance().register(this);

        mTextView.setText(" hello world ,this is test");
    }
}

ok,完成了,可以看到mTextView并没有使用findViewById方法,直接可以使用注解来找到对应的id

其他


在使用apt的时候,有一些相关只是是需要了解的。

Element

元素,可以是方法,可以是类,也可是是属性。可以认为只要能被注解的东西都是元素

常用方法

  • getSimpleName:获取元素名称。比如元素是类,只会获取到类名称
  • getQualifiedName:获取元素全称。比如元素是类,会获取到包名和类
  • asType:返回元素定义的类型,返回结果为TypeMirror
  • getAnnotataion:返回注解。即用该在元素上的注解的类型
  • getKind:返回元素的类型。比如是类,方法还是局部变量等
  • getModifiers:返回该元素上的修饰符。例如,public,final,static…
  • getEnclosedElements:返回该元素封装的元素。比如一个类是个类,那么会返回他的直接子元素(因为在类上,所以会有个构造函数也会一并返回)
  • getEnclosingElement:返回封装该元素的最里层元素

子类

  • ExecutableElement:表示方法,构造方法,初始化器
  • PackageElement:表示包
  • TypeElement:表示类,接口
  • TypeParameterElement:表示类,接口,方法的泛型类型。
  • VariableElement:表示一个字段,enum常量,方法的参数,局部变量以及异常参数

子类中有些特殊的方法不在介绍,直接看文档

TypeMirror

该类中只有一个常用的方法,即getKind,返回类型为TypeKind

TypeKind的作用是判断元素的类型,与Element的getKind方法返回的类型是完全不同的,但是两者在作用上类似。

CodeBlock

创建代码块的,当然也可以自己用StringBuild一个一个字符的敲。

该类支持占位符,使用$符号作为占位符的前缀

占位符

  • $L :没有转义的字面值。可以是字符串,基本数据类型,类型声明,注解以及其他代码块
  • $N :代指的是一个名称。通常使用该占位符通过名称引用另一个声明,如调用方法名称,变量名称等。注意,要使用该占位符,必须要有名称,一般用在应用FieldSpec,MethodSpec,TypeSpec等,因为在声明这些的时候,都要输入一个名字
  • $S :将值转义为字符串,这个与L的区别就是会在值上面加上双引号
  • $T :类型引用
  • $$ :表示美元符号$
  • $W :表示空格或者换行
  • $Z :充当0的宽度空间
  • $> :增加缩进级别
  • $< :减小缩进级别
  • $[ :表示开始声明
  • $] :表示结束声明

其实常用到的也就是android Aspect自定义注编译混淆失效_androidN,android Aspect自定义注编译混淆失效_java_02T

描述基本元素

  • AnnotationSpec :用来创建注解
  • FieldSpec :用来创建字段
  • MethodSpec :用来创建构造函数或者方法
  • ParameterSpec :用来创建方法或者构造函数上的参数
  • TypeSpec :用来创建类,接口或者枚举类