Android APT技术

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 编译器中,具体的步骤如下:

  1. 在你的处理器所在的 Java 库的 main 目录下新建 resources 资源文件夹。
  2. 在上述文件夹下新建 META-INF.services 目录文件夹
  3. 在上述文件夹下创建 javax.annotation.processing.Processor 文件
  4. 在上述文件内写入处理器的全路径名称,多个处理器换行写入。

除此之外,还可以用 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 来获取到它的父元素和子元素,比如我们有 AnimalTypeElement 元素,那么我们可以遍历它的所有的子元素,示例如下:

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,需要依赖 annotationjavapoet 库,依赖如下:

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