Android—APT技术解析

前言

APT,Annotation Processing Tool ,注解处理器

对于Android程序员它真是一个既熟悉又陌生的东西,几乎每个人都知道它,但日常开发工作中几乎不会用到它,但是各种知名框架又几乎全都有它身影。

都知道它是注解处理器,能够配合编译时注解生成文件,减少性能损耗之类的,它但究竟是什么东西呢?

在使用APT之前有很多前置概念要先了解。

什么是编译期?

程序大概可以分为三个时期:

  1. 源码期,开发时编写的Java 或 kotlin代码
  2. 编译期,java 或 kotlin代码 编译成 class字节码文件
  3. 运行期,程序运行 ,字节码文件加载到Java虚拟机中

编译期 就是 java源码 到 class字节码的过程 xxx.java — xxx.class 由javac工具实现

体验javac编译过程

  1. 创建java类 Test.java 随便写点代码
  2. 在当前Java类所在目录 打开cmd
  3. 编写命令 javac Test.java
  4. 如果出错 会由错误提示,没有任何提示表明成功
  5. 查看目录后发现 多出文件 Test.class

上述过程 就是 编译过程,只要是基于Java虚拟机的程序都会有上述步骤,日常开发中开发工具替我们完成编译

编译过程不仅仅这么简单 还有很多可选项

在命令行 打出javac 会出现关于 javac的使用文档

javac

  1. options 可选项
  2. source files 源码文件

重点关注 processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程

processor 代表在编译过程中添加注解处理器 ,注解文档中被翻译为注释

用法: javac <options> <source files>
其中, 可能的选项包括:
  -g                         生成所有调试信息
  -g:none                    不生成任何调试信息
  -g:{lines,vars,source}     只生成某些调试信息
  -nowarn                    不生成任何警告
  -verbose                   输出有关编译器正在执行的操作的消息
  -deprecation               输出使用已过时的 API 的源位置
  -classpath <路径>            指定查找用户类文件和注释处理程序的位置
  -cp <路径>                   指定查找用户类文件和注释处理程序的位置
  -sourcepath <路径>           指定查找输入源文件的位置
  -bootclasspath <路径>        覆盖引导类文件的位置
  -extdirs <目录>              覆盖所安装扩展的位置
  -endorseddirs <目录>         覆盖签名的标准路径的位置
  -proc:{none,only}          控制是否执行注释处理和/或编译。
  -processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
  -processorpath <路径>        指定查找注释处理程序的位置
  -parameters                生成元数据以用于方法参数的反射
  -d <目录>                    指定放置生成的类文件的位置
  -s <目录>                    指定放置生成的源文件的位置
  -h <目录>                    指定放置生成的本机标头文件的位置
  -implicit:{none,class}     指定是否为隐式引用文件生成类文件
  -encoding <编码>             指定源文件使用的字符编码
  -source <发行版>              提供与指定发行版的源兼容性
  -target <发行版>              生成特定 VM 版本的类文件
  -profile <配置文件>            请确保使用的 API 在指定的配置文件中可用
  -version                   版本信息
  -help                      输出标准选项的提要
  -A关键字[=值]                  传递给注释处理程序的选项
  -X                         输出非标准选项的提要
  -J<标记>                     直接将 <标记> 传递给运行时系统
  -Werror                    出现警告时终止编译
  @<文件名>                     从文件读取选项和文件名
复制代码

注解处理器介绍

注解处理器允许开发人员在编译过程中,监听某些某写元素使用了特定注解。

这里使用元素 而不是类的原因是 注解不仅仅会标记在类上 还可以标记在方法,属性等其他元素之上

AbstractProcessor

注解处理器实现需要继承抽象类 javax.annotation.processing.AbstractProcessor 这个抽象类只有一个抽象方法process() ,另外还有两个重要的方法要实现

  1. process
    1. 参数 set,获取到此注解处理器所要处理的注解集合
    2. 参数 RoundEnvironment ,当前编译轮次上下文环境,分析如下:
      1. 为什么说是当前轮次呢,因为可能存在多轮编译 由返回值控制
      2. 重要方法 roundEnvironment.getRootElements(); 观察日志发现,方法返回集合,包含当前编译轮次的所有类
      3. 重要方法 roundEnvironment.getElementsAnnotatedWith(); 传入注解,筛选出所有被注解标记的元素,此次不仅仅是类,方法,属性等等都会被返回
      4. 返回值
        1. true 表明当前注解处理器 新生成类,需要再次进行注解检查,因为新生成的类也可能包含注解也要处理,所以可能会产生多轮次
        2. 返回false 表明没有新生成类,代码没有变化,无需再次处理
    3. 如果注解处理器工作多轮,第一轮已经处理过的类,第二轮不处理,只处理新类
  2. getSupportedAnnotationTypes()
    1. 指定当前注解处理器可以处理那些注解
  3. getSupportedSourceVersion
    1. 指定处理那个版本的代码
  4. 重要的实例变量 ProcessingEnvironment
    1. 提供了一些非常实用的工具类 在注解处理器初始化的时候创建(init()方法执行的时候)
    2. getOptions 接收外部参数
    3. getMessager 日志输出工具
    4. getFiler 用于创建新类
    5. getElementUtils 对元素进行操作的工具类
    6. getTypeUtils 对类型进行操作的工具类

Element 介绍

以下内容转载自 Android APT 系列 (三):APT 技术探究 - 掘金 (juejin.cn

实际上,Java 源文件是一种结构体语言,源代码的每一个部分都对应了一个特定类型的 Element ,例如包,类,字段,方法等等:

package com.dream;         // PackageElement:包元素

public class Main<T> {     // TypeElement:类元素; 其中 <T> 属于 TypeParameterElement 泛型元素

    private int x;         // VariableElement:变量、枚举、方法参数元素

    public Main() {        // ExecuteableElement:构造函数、方法元素
    }
}
复制代码

Java 的 Element 是一个接口,源码如下:

public interface Element extends javax.lang.model.AnnotatedConstruct {
    // 获取元素的类型,实际的对象类型
    TypeMirror asType();
    // 获取Element的类型,判断是哪种Element
    ElementKind getKind();
    // 获取修饰符,如public static final等关键字
    Set<Modifier> getModifiers();
    // 获取类名
    Name getSimpleName();
    // 返回包含该节点的父节点,与getEnclosedElements()方法相反
    Element getEnclosingElement();
    // 返回该节点下直接包含的子节点,例如包节点下包含的类节点
    List<? extends Element> getEnclosedElements();

    @Override
    boolean equals(Object obj);
  
    @Override
    int hashCode();
  
    @Override
    List<? extends AnnotationMirror> getAnnotationMirrors();
  
    //获取注解
    @Override
    <A extends Annotation> A getAnnotation(Class<A> annotationType);
  
    <R, P> R accept(ElementVisitor<R, P> v, P p);
}
复制代码

我们可以通过 Element 获取如上一些信息(写了注释的都是一些常用的)

由 Element 衍生出来的扩展类共有 5 种:

1、PackageElement 表示一个包程序元素

2、TypeElement 表示一个类或者接口程序元素

3、TypeParameterElement 表示一个泛型元素

4、VariableElement 表示一个字段、enum 常量、方法或者构造方法的参数、局部变量或异常参数

5、ExecuteableElement 表示某个类或者接口的方法、构造方法或初始化程序(静态或者实例)

可以发现,Element 有时会代表多种元素,例如 TypeElement 代表类或接口,此时我们可以通过 element.getKind() 来区分:

Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);
for (Element element : elements) {
    if (element.getKind() == ElementKind.CLASS) {
        // 如果元素是类

    } else if (element.getKind() == ElementKind.INTERFACE) {
        // 如果元素是接口

    }
}
复制代码

ElementKind 是一个枚举类,它的取值有很多,如下:

PACKAGE	//表示包
ENUM //表示枚举
CLASS //表示类
ANNOTATION_TYPE	//表示注解
INTERFACE //表示接口
ENUM_CONSTANT //表示枚举常量
FIELD //表示字段
PARAMETER //表示参数
LOCAL_VARIABLE //表示本地变量
EXCEPTION_PARAMETER //表示异常参数
METHOD //表示方法
CONSTRUCTOR //表示构造函数
OTHER //表示其他
复制代码

参数传递

在 app 模块下 build.gradle 文件中 defaultConfig 下声明

javaCompileOptions{
	annotationProcessorOptions{
		arguments = [
                key1: "value1",
                key2: "value2"
	        ]
		}
}
复制代码

在Processor的init方法中获取参数

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    Map<String,String> options = processingEnv.getOptions();
}

复制代码

使用过程

环境搭建

  1. 创建java工程(必须是java工程) apt-complier 用于处理注解
    1. 创建新类 继承 AbstractProcessor
    2. 创建 在main文件夹下创建 resources — META-INF — services — javax.annotation.processing.Processor
    3. 在文件 javax.annotation.processing.Processor 中声明 刚刚创建的注解处理器,继承AbstractProcessor 的类 会有自动提示
  2. 创建Java工程 apt-annotation 用于定义注解
  3. apt-complier 引入 apt-annotation的依赖
  4. Android 工程 引用上述两个工程
    1. 如果是kotlin 工程 需要使用 kapt 不然无法解析 被注解标记的 kotlin文件
implementation project(":apt-annotation")
annotationProcessor project(":apt-complier")
//    kapt project(":apt-complier")  
复制代码

上述套路固定

编写代码

相比于理解apt ,环境搭建和写代码反倒简单。 在动态生成java类之前,肯定会用硬编码将功能实现好,将其中繁琐的重复性逻辑单独抽取出来,结合注解解耦代码。

动态生成代码 肯定是存在一个模板,实现好的逻辑,只需要利用javaPoat 或 字符串拼接 加一点点逻辑改动就好。

只能说这个过程很繁琐麻烦,容易出错,也还是正常开发 而且逻辑都确定了 写好了一个硬编码的版本,不过写代码的方式变了。

生成一个TestActivity,用字符串拼接 和 javapoat 两种方式生成新类,代码如下:

public class RouteProcessor extends AbstractProcessor {

    Filer filer;
    Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//        testCreateFile();
//        testJavaPoet();
        return false;
    }

/**
     *使用JavaPoet
     */
private void testJavaPoet() {

        ClassName AppCompatActivity = ClassName.get("androidx.appcompat.app", "AppCompatActivity");
        ClassName bundle = ClassName.get("android.os", "Bundle");
        ClassName Nullable = ClassName.get("androidx.annotation", "Nullable");
        ClassName Override = ClassName.get("java.lang", "Override");
        //创建一个方法参数
        ParameterSpec savedInstanceState = ParameterSpec.builder(bundle, "savedInstanceState")
                .addAnnotation(Nullable)
                .build();
        //创建一个方法
        MethodSpec onCreate = MethodSpec.methodBuilder("onCreate")
                .addAnnotation(Override)
                .addModifiers(Modifier.PROTECTED)
                .returns(TypeName.VOID)
                .addParameter(savedInstanceState)
                .addStatement("super.onCreate(savedInstanceState)")
                .build();
        //创建一个类
        TypeSpec testActivity = TypeSpec.classBuilder("TestActivity")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(onCreate)
                .superclass(AppCompatActivity)
                .build();

        //创建文件
        JavaFile file = JavaFile.builder("com.whl215.aptdemo", testActivity).build();
        try {
            file.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

/**
     *原生api创建类
*/
private void testCreateFile() {
        BufferedWriter writer = null;
        try {
            JavaFileObject sourceFile = filer.createSourceFile("com.xxx.TestActivity");
            writer = new BufferedWriter(sourceFile.openWriter());

            writer.write("package com.whl215.aptdemo;\n\n");
            writer.write("import android.os.Bundle;\n\n");
            writer.write("import androidx.annotation.Nullable;\n");
            writer.write("import androidx.appcompat.app.AppCompatActivity;\n");
            writer.write("public class TestActivity extends AppCompatActivity {\n\n");
            writer.write("      @Override\n");
            writer.write("      protected void onCreate(@Nullable Bundle savedInstanceState) {\n");
            writer.write("          super.onCreate(savedInstanceState);\n");
            writer.write("    }\n");
            writer.write("}");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

/**
     *可以处理哪些注解
*/
@Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(Route.class.getCanonicalName());
    }

/**
     *处理那个版本的代码
*/
@Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

复制代码

参考文章

Android APT 系列 (三):APT 技术探究 - 掘金 (juejin.cn)

Java进阶--编译时注解处理器(APT)详解 - 掘金 (juejin.cn)

(30条消息) 【Android APT】注解处理器 ( Element 注解节点相关操作 )_韩曙亮的博客-CSDN博客

Java注解之编译时注解 - 掘金 (juejin.cn)

JavaPoet的使用指南 - 掘金 (juejin.cn)

JavaPoet 看这一篇就够了 - 掘金 (juejin.cn)

猜你喜欢

转载自juejin.im/post/7105575660600426509