APT简介
Annotation Processing Tool ,即注解处理器。一般用来处理自定义的注解,然后根据注解生成一个辅助类。最著名的例子就是@BindView注解。
注意,这是在编译时扫描所以继承AbstractProcessor类,然后调用process方法去处理。因为是在编译的时候处理的,所以很多时候需要用到反射。
流程
- 创建一个java library库,用来提供注解
- 创建一个java library库,用来处理注解
- 在android app中引用
总体流程基本上分为这三个部分。但是有些时候,我们会在第2部到第3步之间创建一个android library库,然后在android app中直接引用这个android library
例子
初级
本例只看一下流程,对注解的处理比较粗糙。
-
创建java library库,命名为lib-anno,该库用来定义注解
在该库中创建一个注解
// 当前注解所在包为 com.hhh.lib_anno @Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface BindView { int id(); }
-
再创建一个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方法
-
-
在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的名字。
-
在lib-anno中创建IInjection接口。要求所有被BindView注解元素所在类生成的辅助类都要继承它
package com.hhh.lib_anno; public interface IInjection { void inject(Object activity); }
-
重新写个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(); } } } }
-
创建一个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类的名称以及所在地址
-
在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的宽度空间
- $> :增加缩进级别
- $< :减小缩进级别
- $[ :表示开始声明
- $] :表示结束声明
其实常用到的也就是 L , L, L,N, S , S, S,T
描述基本元素
- AnnotationSpec :用来创建注解
- FieldSpec :用来创建字段
- MethodSpec :用来创建构造函数或者方法
- ParameterSpec :用来创建方法或者构造函数上的参数
- TypeSpec :用来创建类,接口或者枚举类