APT技术简介
在具体了解 APT
技术之前,先简单的对其进行介绍。 APT(Annotation Processing Tool)
是 javac
中提供的一种编译时扫描和处理注解的工具,它会对源代码文件进行检查,并找出其中的注解,然后根据用户自定义的注解处理方法进行额外的处理。 APT
工具不仅能解析注解,还能根据注解生成其他的源文件,最终将生成的新的源文件与原来的源文件共同编译。
注解处理器的声明
public class MineProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
// 被初始化的时候被调用,即入口
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 父类的抽象方法,必须子类实现处理逻辑,如果返回 true,则不要求后续 Processor 处理它们
return true;
}
@Override
public SourceVersion getSupportedSourceVersion() {
// 返回支持的Java版本
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
// 返回支持的注解类型,返回字符串集合,字符串为注解的合法全称
Set<String> annotations = new LinkedHashSet<>();
annotations.add(BindView.class.getCanonicalName());
return annotations;
}
}
注解处理器的注册
我们需要将实现的处理器注册到 Java
编译器中,具体的步骤如下:
- 在你的处理器所在的
Java
库的main
目录下新建resources
资源文件夹。 - 在上述文件夹下新建
META-INF.services
目录文件夹 - 在上述文件夹下创建
javax.annotation.processing.Processor
文件 - 在上述文件内写入处理器的全路径名称,多个处理器换行写入。
除此之外,还可以用 Google
为我们提供的 AutoService
来注解我们的处理器,它会自动生成上述 4 步操作所生成的文件。使用该注解需要依赖 'com.google.auto.service:auto-service:1.0-rc2'
,github
地址 https://github.com/google/auto。
使用代码如下:
@AutoService(Processor.class)
public class MineProcessor extends AbstractProcessor {
....
}
注意:最新版的 studio 3.4.1
版本跟 AutoService
不兼容,需要手写注册。
注解处理器的扫描
在注解处理过程中,我们需要扫描所有的 Java
源文件,源代码的每一个部分都是一个特定类型的 Element
,也就是说 Element
代表源文件中的元素,例如包、类、字段、方法等。
上图所有的接口都继承于 Element
接口,具体每一个类的作用如下:
// 表示混合类型的元素(不仅只有一种类型的Element)
Parameterizable
// 表示一般类、接口、方法或构造方法元素的形式类型参数
TypeParameterElement
// 表示字段、常量、方法或构造函数。参数、局部变量、资源变量或异常参数
VariableElement
// 具有限定名称的元素
QualifiedNameable
// 表示类或接口的方法、构造函数或初始化器(静态或实例),包括注释类型元素。
ExecutableElement
// 表示类和接口
TypeElement
// 表示包
PackageElement
具体的实例如下:
package fanda.zeng.reflect; //PackageElement
public class Animal { //TypeElement
private int mAnimalPrivate; //VariableElement
public Animal() { //ExecutableElement
}
public Animal(int mAnimalPrivate) { //ExecutableElement
this.mAnimalPrivate = mAnimalPrivate;
}
public void animalPublic() { //ExecutableElement
System.out.println("Method : animalPublic");
}
}
我们还可以通过一个 Emlement
来获取到它的父元素和子元素,比如我们有 Animal
的 TypeElement
元素,那么我们可以遍历它的所有的子元素,示例如下:
TypeElement animal= ... ;
// 遍历它的孩子
for (Element e : person.getEnclosedElements()){
// 拿到孩子元素的最近的父元素
Element parent = e.getEnclosingElement();
}
元素种类判断
由上述分析得知,有些元素是混合元素,代表多种种型,如果我们要判断一个元素的种型,应该使用 Element.getKind()
方法配合 ElementKind
枚举类进行判断。
ElementKind
枚举类部分源码如下:
public enum ElementKind {
PACKAGE, //包
ENUM, //枚举
CLASS, //类
ANNOTATION_TYPE, //注解
INTERFACE, //接口
ENUM_CONSTANT, //枚举常量
FIELD, //字段
PARAMETER, //参数
LOCAL_VARIABLE, //局部变量
EXCEPTION_PARAMETER, //异常参数
METHOD, //方法
CONSTRUCTOR, //构造方法
TYPE_PARAMETER, //类型参数
OTHER, //其他
RESOURCE_VARIABLE; //资源变量
}
示例代码如下:
// 获取到所有包含 BindView 注解的元素
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
for (Element element : elements) {
// 判断元素是不是字段类型
if (element.getKind() == ElementKind.FIELD) {
// 打印日志
mMessager.printMessage(Diagnostic.Kind.NOTE, "is_field");
}
}
元素类型判断
当我们知道某个种类后,需要获取更多的信息,可以通过 mirror API
。假如,现在我们已经知道了其为 ElementKind.CLASS
种类,但是我想获取其父类的信息,或知道了其为 ElementKind.METHOD
种类,想获取该方法的返回值类型、参数类型、参数名称。
当我们使用注解处理器时,我们会先找到相应的 Element
,如果你想获得该 Element
的更多的信息,那么可以配合 TypeMirror
使用 TypeKind
来判断当前元素的类型。当然对于不同种类的 Element
,其获取的 TypeMirror
方法可能会不同。
TypeMirror
是一个接口,表示 Java
编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null
类型。还可以表示通配符类型参数、executable
的签名和返回类型,以及对应于包和关键字 void
的伪类型。
TypeKind
枚举声明如下:
BOOLEAN 基本类型 boolean。
INT 基本类型 int。
LONG 基本类型 long。
FLOAT 基本类型 float。
DOUBLE 基本类型 double。
VOID 对应于关键字 void 的伪类型。
NULL null 类型。
ARRAY 数组类型。
PACKAGE 对应于包元素的伪类型。
EXECUTABLE 方法、构造方法或初始化程序。
具体示例如下:
@OnClick(R.id.tv_test)
public void onClick(View view) {
}
Set<? extends Element> elements2 = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
for (Element element : elements2) {
if (element.getKind() == ElementKind.METHOD) {
ExecutableElement executableElement = (ExecutableElement) element;
TypeMirror mirror = executableElement.getReturnType();
mMessager.printMessage(Diagnostic.Kind.NOTE, mirror.getKind().toString());
mMessager.printMessage(Diagnostic.Kind.NOTE, executableElement.getSimpleName());
List<? extends VariableElement> list =executableElement.getParameters();
for (VariableElement variableElement : list) {
mMessager.printMessage(Diagnostic.Kind.NOTE,variableElement.getSimpleName());
}
}
}
输出:
注: VOID
注: onClick
注: view
元素可见性修饰符
在注解处理器中,我们不仅能获得元素的种类和信息,我们还能获取该元素的可见性修饰符(例如public、private等,不包括void)。
具体示例如下:
@OnClick(R.id.tv_test)
public void onClick(View view) {
}
Set<Modifier> modifiers = executableElement.getModifiers();
for (Modifier modifier : modifiers) {
mMessager.printMessage(Diagnostic.Kind.NOTE, modifier.toString());
}
输出:
注: public
注意上述方法只输出了 public
,没有 void
。除 public
外,还有很多其他的修饰符,具体如下:
public enum Modifier {
PUBLIC,
PROTECTED,
PRIVATE,
ABSTRACT,
DEFAULT,
STATIC,
FINAL,
TRANSIENT,
VOLATILE,
SYNCHRONIZED,
NATIVE,
STRICTFP;
private Modifier() {
}
public String toString() {
return this.name().toLowerCase(Locale.US);
}
}
一些工具类
处理器的 init
方法如下:
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
// 被初始化的时候被调用,即入口
}
通常我们会在这个入口方法初始化一些工具类来辅助逻辑处理,那么我们来看一下 ProcessingEnvironment
提供了哪些主要的方法:
// 返回实现Messager接口的对象,用于报告错误信息、警告提醒
Messager getMessager();
// 返回实现Filer接口的对象,用于创建文件、类和辅助文件
Filer getFiler();
// 返回实现Elements接口的对象,用于操作元素的工具类
Elements getElementUtils();
// 返回实现Types接口的对象,用于操作类型的工具类
Types getTypeUtils();
日志打印
Messager
用来报告错误、警告以及提示信息。通常在 init
方法中初始化,如下:
mMessager = processingEnvironment.getMessager();
需要注意的是它并不是处理器开发中的日志工具,而是用来写一些信息给使用此注解库的第三方开发者的。也就是说如果我们像传统的 Java
应用程序抛出一个异常的话,那么运行注解处理器的 JVM
就会崩溃,并且关于 JVM
中的错误信息对于第三方开发者并不是很友好,所以推荐并且强烈建议使用 Messager
。上述示例代码中都有关于 Messager
的使用。
注意:注解处理器是运行它自己的虚拟机 JVM
中,javac
启动一个完整 Java
虚拟机来运行注解处理器。
Messager
的所有方法如下所示:
public interface Messager {
// 打印指定种类的消息
void printMessage(Kind var1, CharSequence var2);
// 在元素的位置上打印指定种类的消息
void printMessage(Kind var1, CharSequence var2, Element var3);
// 在已注解元素的注解镜像位置上打印指定种类的消息
void printMessage(Kind var1, CharSequence var2, Element var3, AnnotationMirror var4);
// 在已注解元素的注解镜像内部注解值的位置上打印指定种类的消息
void printMessage(Kind var1, CharSequence var2, Element var3, AnnotationMirror var4, AnnotationValue var5);
}
使用带有 Element
参数的方法连接到出错的元素,用户可以直接点击错误信息跳到出错源文件的相应行,比如:
F:\my_android_projects\IOCDemo\app\src\main\java\zeng\fanda\com\iocdemo\MainActivity.java:14: 注: is_field
TextView mTest;
文件生成
建议直接使用 JavaPoet
库来构造源文件,github
地址如下: https://github.com/square/javapoet。在 gradle
中你需要添加依赖 'com.squareup:javapoet:1.11.0'
。当进行注释处理或与元数据文件(例如,数据库模式、协议格式)交互时,JavaPoet
对于源文件的生成可能非常有用。通过生成代码,消除了编写样板的必要性,同时也保持了元数据的单一来源。
示例如下:
private void createFileByJavaPoet() {
//创建main方法
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)//设置可见性修饰符public static
.returns(void.class)//设置返回值为void
.addParameter(String[].class, "args")//添加参数类型为String数组,且参数名称为args
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//添加语句
.build();
//创建类
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)//将main方法添加到HelloWord类中
.build();
//创建文件,第一个参数是包名,第二个参数是相关类
JavaFile javaFile = JavaFile.builder("zeng.fanda.com.iocdemo", helloWorld)
.build();
try {
//创建文件
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
}
}
最终生成的 java
文件如下:
package zeng.fanda.com.iocdemo;
import java.lang.String;
import java.lang.System;
public final class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JavaPoet!");
}
}
完整案例
接下来,我们仿着 ButterKnife
来实现控件绑定功能。
一. 创建注解依赖(Java
)库 annotion
,并创建 BindView
注解,代码如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
二. 创建 library
库,里面是工具类,解耦作用,并创建 Butterknife
类,代码如下:
public class Butterknife {
public static void bind(Activity activity) {
String className = activity.getClass().getName() + "$ViewBinder";
try {
Class<?> viewBinderClass = Class.forName(className);
Method binder = viewBinderClass.getMethod("bindView",activity.getClass());
binder.invoke(viewBinderClass.newInstance(), activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
三. 创建 compiler
处理器库,用于编译时生成 Java
文件,并创建 ButterKnifeProcessor
,需要依赖 annotation
和 javapoet
库,依赖如下:
implementation project(':annotation')
implementation 'com.squareup:javapoet:1.11.0'
代码如下:
public class ButterKnifeProcessor extends AbstractProcessor {
private Elements mElementsUtils;
private Filer mFiler;
private Map<TypeElement, Set<Element>> mElems;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
mElementsUtils = processingEnvironment.getElementUtils();
mFiler = processingEnvironment.getFiler();
mElems = new HashMap<>();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
boolean hasError = verifyGeneratedCode(roundEnvironment.getElementsAnnotatedWith(BindView.class));
if (!hasError) {
initBindElems(roundEnvironment.getElementsAnnotatedWith(BindView.class));
generateJavaClass();
}
return true;
}
/**
* public final class MainActivity$ViewBinder {
* public static void bindView(MainActivity target) {
* target.mIOC = (android.widget.TextView)target.findViewById(2131165325);
* target.mOtherIOC = (android.widget.TextView)target.findViewById(2131165326);
* }
* }
*/
private void generateJavaClass() {
for (TypeElement enclosedElem : mElems.keySet()) {
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(ClassName.get(enclosedElem.asType()), "target")
.returns(TypeName.VOID);
for (Element bindElem : mElems.get(enclosedElem)) {
methodSpecBuilder.addStatement(String.format("target.%s = (%s)target.findViewById(%d)", bindElem.getSimpleName(), bindElem.asType(), bindElem.getAnnotation(BindView.class).value()));
}
TypeSpec typeSpec = TypeSpec.classBuilder(enclosedElem.getSimpleName() + "$ViewBinder")
.addModifiers(Modifier.FINAL, Modifier.PUBLIC)
.addMethod(methodSpecBuilder.build())
.build();
JavaFile file = JavaFile.builder(getPackageName(enclosedElem), typeSpec).build();
try {
file.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取包路径
*/
private String getPackageName(TypeElement enclosedElem) {
return mElementsUtils.getPackageOf(enclosedElem).getQualifiedName().toString();
}
/**
* 初始化绑定元素
*/
private void initBindElems(Set<? extends Element> bindElems) {
for (Element bindElem : bindElems) {
TypeElement enclosedElem = (TypeElement) bindElem.getEnclosingElement();
Set<Element> elems = mElems.get(enclosedElem);
if (elems == null) {
elems = new HashSet<>();
mElems.put(enclosedElem, elems);
}
elems.add(bindElem);
}
}
/**
* 校验代码生成限制
*/
private boolean verifyGeneratedCode(Set<? extends Element> bindElems) {
boolean hasError = false;
for (Element bindElem : bindElems) {
TypeElement enclosingElement = (TypeElement) bindElem.getEnclosingElement();
// 校验修饰符,不能是静态和私有
Set<Modifier> modifiers = bindElem.getModifiers();
if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC)) {
hasError = true;
}
//检验容器类型(接口、类等等),只能用于类类型
if (enclosingElement.getKind() != ElementKind.CLASS) {
hasError = true;
}
// 校验容器类的修饰符,不能是私有类
if (enclosingElement.getModifiers().contains(Modifier.PRIVATE)) {
hasError = true;
}
}
return hasError;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new LinkedHashSet<>();
set.add(BindView.class.getCanonicalName());
set.add(OnClick.class.getCanonicalName());
return set;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
四. 注册处理器,创建对应的目录和文件,在 javax.annotation.processing.Processor
文件里面,写入 zeng.fanda.com.compiler.ButterKnifeProcessor
五. 在主项目 APP
模块下,加上如下依赖:
implementation project(':annotation')
implementation project(':library')
annotationProcessor project(':compiler')
六. 最后在界面控件声明时,写上注解进行绑定,然后在界面初始化时调用 Butterknife.bind(this)
即可,代码如下:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_ioc)
TextView mIOC;
@BindView(R.id.tv_other_ioc)
TextView mOtherIOC;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Butterknife.bind(this);
}
}
断点调试
一. 打开 Edit Configuration
项,然后点击左上角的 +
按钮并选择 Remote
项,然后为这个配置设置一个命名、端口和调试路径,如下图:
二. 复制上图步骤 3
的内容,这段内容是启动可远程调试的JVM的参数,打开 IDE
右边的 Gradle
小窗口,选择对应的 task
,右键选中 Create Configuration
,如下图:
三. 将刚才复制的参数粘贴到VM options
对应的编辑框中,并将 suspend=n
参数改为 suspend=y
。
四. 启动我们要调试的程序,如下图,双击运行:
Run Build
一直处于等待状态,表示远程 JVM
已启动并在等待调试器连接。
五. 选中我们刚才设置的 Remote
配置的名字,然后点击 Debug
按钮启动调试器,,如下图:
六. 成功进入代码断点,如下图:
具体参考该文章,https://blog.csdn.net/tomatomas/article/details/53998585