1. APT简介
1.1 什么是APT?
APT(Annotation Processing Tool)即注解处理器,它是一种处理注解的工具,也是javac中的一个工具。
APT可以用来在编译时扫描和处理注解。
1.2 APT的作用
通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。在Android中有如ButterKnife、Dagger、EventBus等第三方框架,都采用了APT。
注意,获取注解及生成代码都是在代码编译时候完成的,相比反射在运行时处理注解大大提高了程序性能。
2. 创建APT的项目结构
2.1 创建Android Module命名为app(主工程)
dependencies {
...
// 导入自定义注解
implementation project(":apt-annotation")
// 指定注释处理器
annotationProcessor project(":apt-compiler")
...
}
2.2 创建Java library Module命名为 apt-annotation
存放自定义注解
gradle配置文件如下:
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
2.3 创建Java library Module命名为 apt-compiler
依赖 apt-annotation
、auto-service
gradle配置文件如下:
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":apt-annotation")
// 注册注解,并对其生成META-INF的配置信息
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
// 第三方自动生成代码框架(可以通过类的调用方式来自动生成代码,简洁高效)
implementation 'com.squareup:javapoet:1.10.0'
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
为什么两个模块一定要是Java Library而不是Android Library?
- 如果创建Android Library模块会发现不能找到AbstractProcessor这个类,这是因为Android平台是基于OpenJDK的,而OpenJDK中不包含APT的相关代码。因此,在使用APT时,必须在Java Library中进行。
3. AbstractProcessor分析(核心)
AbstractProcessor是一个抽象类,需要通过继承它来实现我们自己的注解解释器
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
/**
* 注解处理器的初始化阶段,可以通过ProcessingEnvironment来获取一些帮助我们来处理注解的工具类
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
/**
* 指明有哪些注解需要被扫描到,返回注解的全路径(包名+类名)
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
/**
* 用来指定当前正在使用的Java版本,一般返回SourceVersion.latestSupported()表示最新的java版本即可
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
/**
* 核心方法,注解的处理和生成代码都是在这个方法中完成
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
return false;
}
}
-@AutoService
的作用是用来生成META-INF/services/javax.annotation.processing.Processor
文件,并且自动将该注解标记的注解处理器添加到该文件中。
- 在注解处理器的初始化阶段,可以通过参数
ProcessingEnvironment
来获取一些帮助我们来处理注解的工具类
// Element操作类
Elements elementUtils = processingEnv.getElementUtils();
// 类信息工具类
Types typeUtils = processingEnv.getTypeUtils();
// 日志工具类
Messager messager = processingEnv.getMessager();
// 文件工具类
Filer filer = environment.getFiler();
3.1 了解Element
在Java中Element是一个接口,表示一个程序元素,它可以指代包、类、方法或者一个变量。Element已知的子接口有如下几种:
-
PackageElement
表示一个包程序元素。提供对有关包及其成员的信息的访问。 -
ExecutableElement
表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。 -
TypeElement
表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。 -
VariableElement
表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
我们就拿一个简单的类举例,就可以很容易理解这些元素。
不同类型Element其实就是映射了Java中不同的类元素
package com.aptdemo.user; // PackageElement
public class User { // TypeElement
private long id; // VariableElement
private String username; // VariableElement
public long getId() { // ExecutableElement
return id;
}
public void setId( // ExecutableElement
long id) { // VariableElement
this.id = id;
}
public String getUsername() { // ExecutableElement
return username;
}
public void setUsername( // ExecutableElement
String username) { // VariableElement
this.username = username;
}
}
4. APT实例讲解
仿造ButterKnife中的 @BindView 注解,自动生成findViewById代码
4.1 在apt-annotation模块中自定义注解 @BindView
@Target(ElementType.FIELD) // 作用于成员属性上
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
4.2 在apt-compiler模块中,编写相关的注解处理逻辑
解析相关的节点信息存放到NodeInfo
中
public class NodeInfo {
/**
* 包路径
*/
private String packageName;
/**
* 节点所在类名称
*/
private String className;
/**
* 节点类型名称
*/
private String typeName;
/**
* 节点名称
*/
private String nodeName;
/**
* 注解的value
*/
private int value;
public NodeInfo(String packageName, String className, String typeName,
String nodeName, int value) {
this.packageName = packageName;
this.className = className;
this.typeName = typeName;
this.nodeName = nodeName;
this.value = value;
}
public String getPackageName() {
return packageName;
}
public String getClassName() {
return className;
}
public String getTypeName() {
return typeName;
}
public String getNodeName() {
return nodeName;
}
public int getValue() {
return value;
}
}
编写核心的注解处理器
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
/**
* Element操作类
*/
private Elements mElementUtils;
/**
* 类信息工具类
*/
private Types mTypeUtils;
/**
* 日志工具类
*/
private Messager mMessager;
/**
* 文件创建工具类
*/
private Filer mFiler;
/**
* 节点信息缓存
*/
private Map<String, List<NodeInfo>> mCache = new HashMap<>();
/**
* 注解处理器的初始化阶段,可以通过ProcessingEnvironment来获取一些帮助我们来处理注解的工具类
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mElementUtils = processingEnv.getElementUtils();
mTypeUtils = processingEnv.getTypeUtils();
mMessager = processingEnv.getMessager();
mFiler = processingEnv.getFiler();
}
/**
* 指明有哪些注解需要被扫描到,返回注解的全路径(包名+类名)
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(BindView.class.getCanonicalName());
}
/**
* 用来指定当前正在使用的Java版本,一般返回SourceVersion.latestSupported()表示最新的java版本即可
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 核心方法,注解的处理和生成代码都是在这个方法中完成
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations == null || annotations.isEmpty()) return false;
// 获取所有 @BindView 节点
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
if (elements == null || elements.isEmpty()) return false;
// 遍历节点
for (Element element : elements) {
// 获取节点包信息
String packageName = mElementUtils.getPackageOf(element).getQualifiedName().toString();
// 获取节点类信息,由于 @BindView 作用于成员属性上,所以这里使用 getEnclosingElement() 获取父节点信息
String className = element.getEnclosingElement().getSimpleName().toString();
// 获取节点类型
String typeName = element.asType().toString();
// 获取节点标记的属性名称
String nodeName = element.getSimpleName().toString();
// 获取注解的值
int value = element.getAnnotation(BindView.class).value();
// 打印
mMessager.printMessage(Diagnostic.Kind.NOTE, "packageName:" + packageName);
mMessager.printMessage(Diagnostic.Kind.NOTE, "className:" + className);
mMessager.printMessage(Diagnostic.Kind.NOTE, "typeName:" + typeName);
mMessager.printMessage(Diagnostic.Kind.NOTE, "nodeName:" + nodeName);
mMessager.printMessage(Diagnostic.Kind.NOTE, "value:" + value);
// 缓存KEY
String key = packageName + "." + className;
// 缓存节点信息
List<NodeInfo> nodeInfos = mCache.get(key);
if (nodeInfos == null) {
nodeInfos = new ArrayList<>();
nodeInfos.add(new NodeInfo(packageName, className, typeName, nodeName, value));
// 缓存
mCache.put(key, nodeInfos);
} else {
nodeInfos.add(new NodeInfo(packageName, className, typeName, nodeName, value));
}
}
// 判断临时缓存是否不为空
if (!mCache.isEmpty()) {
// 遍历临时缓存文件
for (Map.Entry<String, List<NodeInfo>> stringListEntry : mCache.entrySet()) {
try {
// 创建文件
createFile(stringListEntry.getValue());
} catch (Exception e) {
e.printStackTrace();
}
}
}
return false;
}
/**
* 创建文件,自动生成代码
*/
private void createFile(List<NodeInfo> infos) throws IOException {
NodeInfo info = infos.get(0);
// 生成的文件名(类名)
String className = info.getClassName() + "$$ViewBinding";
// 方法参数
ParameterSpec parameterSpec = ParameterSpec.builder(
ClassName.get(info.getPackageName(), info.getClassName()), "target")
.build();
// 方法
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(parameterSpec)
.returns(void.class);
// 给方法添加代码块
for (NodeInfo nodeInfo : infos) {
// target.textView = (TextView) target.findViewByID(R.id.text_view);
methodSpecBuilder.addStatement("target.$L = ($L)target.findViewById($L)",
nodeInfo.getNodeName(),
nodeInfo.getTypeName(),
nodeInfo.getValue());
}
// 类
TypeSpec typeSpec = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addMethod(methodSpecBuilder.build())
.build();
// 生成文件
JavaFile.builder(info.getPackageName(), typeSpec)
.build()
.writeTo(mFiler);
}
}
javapoet的具体使用教程请参考:javapoet官方文档
4.3 在app模块中应用注解
在MainActivity中使用注解 @BindView
@BindView(R.id.text_view)
TextView textView;
然后再Rebuild Project,这样就可以在build->generated->ap_generated_sources->debug->包名
路径下,看见自动生成的文件MainActivity$$ViewBinding
。
APT自动生成的文件MainActivity$$ViewBinding
代码如下:
public class MainActivity$$ViewBinding {
public static void bind(MainActivity target) {
target.textView = (android.widget.TextView)target.findViewById(2131165354);
}
}
这样我们就可以仿造ButterKnife,在App模块下新建一个ButterKnife类,利用反射来调用上面方法
public class ButterKnife {
public static void bind(Activity target) {
try {
Class<?> clazz = target.getClass();
// 反射获取apt生成的指定类
Class<?> bindViewClass = Class.forName(clazz.getName() + "$$ViewBinding");
// 获取它的方法
Method method = bindViewClass.getMethod("bind", clazz);
// 执行方法
method.invoke(bindViewClass.newInstance(), target);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里我们的BindView注解功能就大功告成了
public class MainActivity extends AppCompatActivity {
@BindView(R.id.text_view)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
textView.setText("Hello BindView");
}
}