APT即注解处理器(Annotation Processing Tool)的简称
简单来说就是个javac的一个工具,可以在代码编译的阶段扫描注解,然后做你想干的事情 比生成代码文件、实现一些功能等等…很多开源框架都应用了这一技术如:Butter Knife、Dagger等等…
一、这篇文章通过实现和Butter Knife一样的自动findViewByid()功能,来了解整个APT的过程
1.先来看下整个项目的模块和依赖关系
-
findview
是个Android的Library:处理找控件的具体操作 -
findview-annotation
是一个Java的Library:用来存放注解类 -
findview-compiler
是一个Java的Library:用来存放注解处理器的 -
sample
是一个Android项目:这里就是用来写示例代码的了
2.模块之间的依赖关系如下
-
findview
不依赖其他Module -
findview-annotation
不依赖其他Module -
findview-compiler
依赖findview-annotation
-
sample
依赖findview
、findview-annotation
、findview-compiler
二、那就开始来把Module一个个创建了
1.首先创建 findview-annotation
模块并创建一个BindView
注解类
//作用在属性之上
@Target(ElementType.FIELD)
//编译期
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
- @Target()这个属性表明注解可以作用于类,方法,变量,参数…等等;这里表明作用于变量之上
- @Retention()这个属性表明注解需要在什么时期保留,这里表明保留到class文件中
- RetentionPolicy.SOURCE:保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
- RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃
- RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
三、继续创建findview-compiler
模块并创建一个注解处理器
- 通过菜单
File —> New Module —> 选择Java Library
创建即可 - 还需要依赖google提供的两个注解处理器工具,gradle文件内容如下
- 同时依赖
findview-annotation
模块
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//google
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
//依赖注解Module
implementation project(path: ':findview-annotation')
}
//解决乱码
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
sourceCompatibility = "7"
targetCompatibility = "7"
1.创建一个FindViewProcessor
处理器来处理上面定义的BindView
注解
@AutoService(Processor.class)
//需要扫描哪些注解
@SupportedAnnotationTypes("com.azhon.findview.annotation.BindView")
//指定jdk的编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class FindViewProcessor extends AbstractProcessor {
//操作Element工具
private Elements elementUtils;
//类信息工具
private Types typeUtils;
//日志工具
private Messager messager;
//文件生成工具
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
typeUtils = processingEnv.getTypeUtils();
messager = processingEnv.getMessager();
filer = processingEnv.getFiler();
//打印日志
messager.printMessage(Diagnostic.Kind.NOTE, "FindViewProcessor");
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
- @AutoService(Processor.class) 表示这个是一个注解处理器,这样当编译项目的时候这个地方的代码就会去执行了
- @SupportedAnnotationTypes(“com.azhon.findview.annotation.BindView”)需要扫描的注解类路径
- @SupportedSourceVersion(SourceVersion.RELEASE_8)指定jdk的编译版本
2.让sample
示例代码的gradle去依赖注解和注解处理器模块
- sample/build.gradle文件
implementation project(path: ':findview-annotation')
//依赖注解处理器
annotationProcessor project(path: ':findview-compiler')
3.点击菜单的Build —> Rebuild Project
就可以在看到打印的日志了,如下:
到这里说明注解处理器已经正常工作了,接下来就只要去扫描我们自定义的注解然后做自己想干的事情即可
4.要想注解处理器扫描得到注解,我们就得先去使用它
- 在
sample
模块中的build.gradle
文件依赖注解和注解处理器
//依赖注解
implementation project(path: ':findview-annotation')
//依赖注解处理器
annotationProcessor project(path: ':findview-compiler')
- 使用就就很简单了,布局就一个
TextView
然后给个id为tv
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- 在MainActivity中使用自定义的注解
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
5.回到FindViewProcessor
处理器中,通过代码找到的我们上面标记的TextView
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//获取所有使用到注解的节点
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
//遍历所有的类节点
for (Element element : elements) {
//类的的包名
String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
//类名
String clsName = element.getEnclosingElement().getSimpleName().toString();
String simpleName = element.getSimpleName().toString();
int value = element.getAnnotation(BindView.class).value();
messager.printMessage(Diagnostic.Kind.NOTE, "packageName:" + packageName);
messager.printMessage(Diagnostic.Kind.NOTE, "clsName:" + clsName);
messager.printMessage(Diagnostic.Kind.NOTE, "simpleName:" + simpleName);
messager.printMessage(Diagnostic.Kind.NOTE, "value:" + value);
}
return false;
}
- Rebuild之后(value就是R.id.tv的值)
注解所在的包名、类名、控件名称都有了就可以开始生成找ID的代码了;也就是生成一个Java文件然后实现找ID的代码
6.一个Java类的结构是从上到下一般是由 包名、导包、类名、变量、方法等组成,所以我们生成类时也需要这样来生成
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//获取所有使用到注解的节点
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
//遍历所有的类节点
for (Element element : elements) {
//类的的包名
String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
//类名
String clsName = element.getEnclosingElement().getSimpleName().toString();
String simpleName = element.getSimpleName().toString();
int value = element.getAnnotation(BindView.class).value();
try {
createJavaFile(packageName, clsName, simpleName, value);
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
/**
* 生成java文件
*
* @param packageName
* @param clsName
* @throws IOException
*/
private void createJavaFile(String packageName, String clsName, String simpleName, int value)
throws IOException {
String className = clsName + "$$ViewBinding";
JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + className);
Writer writer = sourceFile.openWriter();
writer.write("package " + packageName + ";\n");
writer.write("import android.view.View;\n");
writer.write("public class " + className + "{\n");
writer.write("public " + className + "(" + clsName + " target){\n");
writer.write("this(target,target.getWindow().getDecorView());\n}\n");
writer.write("public " + className + "(" + clsName + " target, View view){\n");
writer.write("target." + simpleName + "=view.findViewById(" + value + ");\n }");
writer.write("}");
writer.close();
}
- 再次Rebuild之后就可以看到生成的文件了,在
sample
的build\generated\ap_generated_sources\debug\out\com\azhon\sample\
下
四、注解和自动生成找ID的代码都已经生成好了,所以最后一步就是去使用了;那么现在就来看看要怎么使用
- 在把
findview
模块创建出来,在这里实现调用逻辑;并创建一个FindView类
- 通过菜单
File —> New Module —> 选择Java Library
创建即可
public class FindView {
public static void bind(Activity activity) {
Constructor constructor = findBindingConstructorForClass(activity.getClass());
if (constructor != null) {
try {
//实例画APT生成的类 即会自动找id
constructor.newInstance(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 根据当前类名找到APT生成的类
*
* @param cls
* @return
*/
public static Constructor findBindingConstructorForClass(Class<?> cls) {
String clsName = cls.getName();
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "$$ViewBinding");
return bindingClass.getConstructor(cls);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
- APT生成的类是我们自定义的规则
Activity类名+$$ViewBinding
,所以可以通过类加载机制加载到每个Activity对应生成的类 - 通过反射实例化类的构造方法传入对应的参数,这样就调用到了找ID的代码
1.sample
模块依赖FindView
模块
implementation project(path: ':findview')
- 然后使用即可,与ButterKnife使用是一致的
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FindView.bind(this);
textView.setText("成功设置了文本");
}
}
效果图
FindViewDemo下载地址