1.APT注解处理器
APT是Annotation Processing Tool的简称,即注解处理器。它是一种处理注解的工具,可以在代码编译期扫描并解析注解,最终生成处理注解逻辑的Java文件,从而减少手动的代码输入。ButterKnife、EventBus、ARouter、GreenDAO都使用了APT技术。
使用APT可以在编译时处理注解,生成额外的Java文件,有如下效果:
①可以达到减少重复代码手工编写的效果。如ButterKnife,可以直接使用注解来减少findviewbyid这些代码,只需要通过注解表示是哪个id就够了。
②功能封装。将主要的功能逻辑封装起来,只保留注解调用。
③相对于使用Java反射来处理运行时注解,使用APT有着更加良好的性能。
APT的作用时间是编译时。首先要了解Android中代码编译流程:Java—>class —> dex,代码最终生成dex文件打入到APK包里面。
①APT是在编译开始时就介入的,用来处理编译时注解。
②AOP(Aspect Oridnted Programming)是在编译完成后生成dex文件之前,通过直接修改.class文件的方式,来对代码进行修改或添加逻辑。常用在在代码监控,代码修改,代码分析这些场景。
2.APT用法
用APT实现一个简单的功能:只要在任何类的成员变量上添加一个@Print注解,就可以动态生成一个方法,然后把成员变量的变量名输出:
动态生成的类大概长这样:
APT使用的流程如下:
①首先需要创建两个JavaLibrary,一个用来创建自定义注解;一个用来创建注解处理器,扫描注解;
②获取到添加注解的成员变量名,处理java文件生成逻辑;
③动态生成类和方法,用IO生成文件。
1)首先配置APT:
①用Android Studio创建一个空项目,然后创建两个JavaLibrary,一个注解的Lib: apt-annotation,一个扫描注解的Lib: apt-processor。
创建完之后:
②app模块依赖两个Library
implementation project(path: ':apt-annotation')
annotationProcessor project(path: ':apt-processor')
③在注解Lib中创建一个注解类
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Print {
}
④注解处理器的注册
自定义的注解处理器必须经过注册才能够使用,可以使用Google autoService来进行注解处理器的自动注册。
首先需要在注解处理器apt-processor所在module的build.gradle文件添加autoService的包引入,即扫描注解的Lib添加依赖:
dependencies {
//自动注册,动态生成 META-INF/...文件
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
//依赖apt-annotation
implementation project(path: ':apt-annotation')
}
⑤创建扫描注解的类,将自动注册的注解@AutoService添加到注解处理器上以实现自动注册效果。
⑥重写init方法,输出Hello,APT
AbstractProcessor的init方法提供了一个环境对象processingEnv,从这个对象中可以得到一系列的工具以及获取到外部传入的参数。
注意: 这里是JavaLib,所以不能使用Log打印,这里可以使用Java的println()或注解处理器提供的方法,建议使用注解处理器提供的。
现在已经完成了APT的基本配置,可以build一下项目了,会成功输出Hello,APT
注意:
如果成功输出了文本,说明APT已经配置好,可以继续下一步了。
如果失败了:
①如果继承的时候找不到AbstractProcessor类,那创建的肯定不是JavaLibrary,可以删掉重新创建。
②如果点击编译没反应,可以试试先clear一下项目再重新编译。
③如果都不行,就去检查一下前面流程的依赖是否都配置正确。
2)继续完成功能
现在可以继续完成上面要实现的功能了。
①需要先来实现几个方法:
/**要扫描的注解,可以添加多个 */
@Override
public Set
HashSet
hashSet.add(Print.class.getCanonicalName());
return hashSet;
}
/** 编译版本,固定写法就可以*/
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}
② 定义注解
先在MianActivity中添加两个成员变量,并使用定义的注解
③解析注解
真正解析注解的地方是在process方法,先试试能不能拿到被注解的变量名:
/* 扫描注解回调 */
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
//拿到所有添加Print注解的成员变量
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(Print.class);
for (Element element : elements) {
Name simpleName = element.getSimpleName(); //拿到成员变量名
processingEnv.getMessager( ).printMessage(Diagnostic.Kind.NOTE,simpleName); //输出成员变量名
}
return false;
}
process方法有两个参数,第一个参数表示需要处理的注解的集合,即创建自定义注解处理器时MyProcessor中所定义的类。第二个参数是APT框架提供的查询程序元素的工具,如通过roundEnv.getElementsAnnotatedWith可以查询到程序中所有标注了某注解的类。
现在编译试一下:
④生成类
既然能拿到被注解的变量名,后面就简单了,只需要用字符串拼出来一个工具类,然后用IO流写到本地就ok了。
现在点击一下编译,可以看到app模块下的build文件已经有生成的类了
⑤调用方法
现在回到MainActivity,就可以直接调用这个动态生成的类了
3.javaPoet
JavaPoet可以以面向对象的思维来生成类,这样就不用手动拼接字符串的方式来生成类了,优化一下上面的代码:
先添加依赖
implementation 'com.squareup:javapoet:1.13.0'
/**扫描注解回调*/
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
//拿到所有添加Print注解的成员变量
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(Print.class);
//生成类
TypeSpec.Builder classBuilder = TypeSpec
.classBuilder("PrintUtil")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
for (Element element : elements) {
//拿到成员变量名
Name simpleName = element.getSimpleName();
//生成方法
MethodSpec method = MethodSpec.methodBuilder("print$$"+simpleName)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
classBuilder.addMethod(method);
}
//包
JavaFile javaFile = JavaFile
.builder("com.lkx.helloapt", classBuilder.build())
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
编译一下即可生成PrintUtil类
4.总结
①APT可以在编译时扫描注解,提前生成类
②JavaPoet可以优雅的生成类,再也不用拼接了
③APT最主要的功能就是可以替代反射的一些功能,避免降低性能
④APT只会在编译时影响一点点速度,在运行期不会,而反射刚好相反。