IT@程序员猿媛手机移动程序开发程序员技术栈

Android - 一篇崭新的 APT 教程 AS3.4 Gra

2019-05-10  本文已影响4人  拾识物者
Pixabay License

网上有很多 APT 相关教程,最近开始学这个,发现有一些内容已经过时了,在使用过程中也发现了一些坑,总结一下,形成这篇教程。

本文开发环境:2019年5月初最新版本的 Android Studio 3.4、Android Plugin 3.4.0、Gradle 5.1.1。

本教程需要读者了解注解 Annotation 的基本知识,不涉及 Annotation 运行时反射的用法,专注于自定义 APT 的流程和步骤,以及使用新版 AS 和 gradle 的注意事项。

简介

APT,Annotation Processing Tool,注解处理工具,是 JDK 提供的一个工具。注意是 Java 语言支持的,不是安卓特有的东西,这点对于理解 APT 有一些作用。早期的 JDK 提供了一个单独的 apt 程序,后来被整合到 javac 中了。它最常见的用法就是根据注解自动生成源代码,很多流行的库都使用了注解处理器来生成代码,比如 ButterKnife 会生成资源与变量绑定的代码,让开发者不用手写繁琐重复的 findViewById。

原理

那么 javac 是怎么使用 APT 生成代码的呢?javac 并不知道你想怎么生成代码,需要你按照 javac 提供的规则和接口来自定义 Annotation Processor。注意这是 Java 语言定义的规则。

接口

javax.annotation.processing.AbstractProcessor,实现这个抽象类,在 process() 方法中自定义生成代码的细节。可以称它为注解处理器 Annotation Processor。只有这一个类型作为接口,当然类中还有一些其他方法用来设置 Annotation Processor 的属性。

规则

安卓和 gradle

以上是 Java 的基础规则,到了 gradle 中就要按照 Java Plugin 的语法和规则来配置。gradle 一直致力于提高编译速度,在新版的 gradle 5.+ 中,为了推行更快速的增量编译,关闭了一个默认功能,导致由 gradle 4.+ 升级上来的项目有可能构建失败,这其中的弯弯绕绕和坑坑洼洼在下面的步骤中详细讲解。

步骤

1. 项目架构

分 3 个模块:

为什么要分这么多模块?其中 app 模块是用来测试的,测试新定义的 Annotation Processor 能否成功运行。annotation + compiler 如果写得糙一点可以合并在一起,例如谷歌的 auto service。但两者的目的并不一样,annotation 是专门定义注解的,而 compiler 是处理注解的。最重要的是,annotation(RetentionPolicy.CLASS,RetentionPolicy.RUNTIME)是需要被编译到 app 项目的 class 文件中的,而 compiler 没有必要进入 app 中。

2. annotation 模块

该模块是定义注解用的,可以包含多个注解,例如 ButterKnife 就有二十多个注解定义。而且只有注解的定义,没有其他任何代码。这样做的原因主要是在架构上能单独隔离一个完整内聚的功能,可以被其他模块引用,比如 app 模块必须引用,compiler 模块可以引用。

创建模块

annotation 模块中只有注解定义,可以直接定义为 java library,而不用定义为 android library,定义为安卓库反而会限制它被其他 java library 引用。

build.gradle

apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

然后就可以在这个模块中添加注解定义了。

注解简介

定义注解使用 @interface 关键字,然后使用元注解 @Target@Retention 定义注解的修饰目标和保留策略:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface NiceField {}

当注解被 Annotation Processor 处理的时候,其实这三种策略的注解都是可以处理的。比如谷歌的 auto service 中的 @AutoService 就是 RetentionPolicy.SOURCE 类型的,用完即抛。如果还有其他运行时处理的需求,可以使用 RetentionPolicy.RUNTIME

3. compiler 模块

该模块编译 Annotation Processor 代码,并将其打包成 jar 文件供其他模块使用,一般都起名为 compiler 或 processor。

创建模块

上文说过只要将 jar 放置在了 classpath 中,javac 就会自动查找并执行处理代码。因此只需要创建一个 java library 模块:

build.gradle

apply plugin: 'java-library'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
    implementation 'com.squareup:javapoet:1.10.0' // 使用 javapoet 生成 .java 文件
    implementation project(':annotation') // 依赖 annotation 模块方便引用其中的注解
    compileOnly 'com.google.auto.service:auto-service:1.0-rc5'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc5'
}

最后两行的依赖是谷歌的 auto service。为啥两行后面是一样的?

谷歌 auto service 和 META-INF

谷歌的 auto service 也是一种 Annotation Processor,它能自动生成 META-INF 目录以及相关文件,避免手工创建该文件,手工创建有可能失误(我就写错过路径😂)。使用 auto service 中的 @AutoService(Processor.class) 注解修饰 Annotation Processor 类就可以在编译过程中自动生成文件。

可见 auto service 是给 Annotation Processor 服务的 Annotation Processor,是不是很有趣。可能有人要问 auto service 的 META-INF 目录和文件怎么办?不是还可以手工写嘛。

手工怎么写这个 META-INF? 在 jar 包中路径是
META-INF/service/javax.annotation.processing.Processor,见下图:

但在项目中应该放在哪里呢?见下图:

手工生成这个 META-INF 目录和文件就不用引入 auto service 依赖了。

如果引入的话,还要注意有两个配置 compileOnlyannotationProcessor

    compileOnly 'com.google.auto.service:auto-service:1.0-rc5'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc5'

这两个重复写的原因就是 auto service 并没有将 annotation 和 processor 分割成两个项目,而是混到了一起。

定制化 Processor

两个部分:

  1. 定义文件生成规则
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // 因为运行在 javac 执行过程中,打印必须使用 messager,而不能使用 System.out
    messager.printMessage(Diagnostic.Kind.NOTE, "processing");
    // 此处正经工具应该使用参数 set 和 roundEnvironment 根据注解的具体使用情况来生成代码
    // 本文主要讲步骤和配置,不涉及这个部分。仅生成了一个独立的 java 文件。
    MethodSpec main = MethodSpec.methodBuilder("main")
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
            .returns(void.class)
            .addParameter(String[].class, "args")
            .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
            .build();
    TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addMethod(main)
            .build();
    JavaFile javaFile = JavaFile.builder("com.example.study.app", helloWorld)
            .build();
    try {
        // 最后要将内容写入到 java 文件中,这里必须使用 processingEnv 中获取的 Filer 对象
        // 它会自动处理路径问题,我们只需要定义好包名类名和文件内容即可。
        Filer filer = processingEnv.getFiler();
        javaFile.writeTo(filer);
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 返回值表示处理了 set 参数中包含的所有注解,不会再将这些注解移交给编译流程中的
    // 其他 Annotation Processor。一般都不会有多个 Annotation Processor,一般都写 true。
    return true;
}
  1. 配置 Processor。有两种配置方法:可以用注解的方式也可以重写 AbstractProcessor 的某些方法。如果两种方式都定义,则会使用方法的版本,但从工程的角度应该只用一种方法来设置,以免混淆。这些注解设置为了 RetentionPolicy.RUNTIME 类型,如果不重写相关方法,AbstractProcessor 中方法的默认实现则会使用反射来获取注解中设置的值。
// 注解方式
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.study.annotation.NiceField")
public class MyProcessor extends AbstractProcessor {
    // 重写方法方式
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(NiceField.class.getCanonicalName());
    }
}

4. app 模块

该模块用来测试和验证 Annotation Processor,是一个简单的 android application 模块。

创建模块

就不贴图了,默认一个 android 项目就会有一个 app 模块,看下面的 build.gradle:

// 前半部分都是自动生成的不用细看
apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.example.study.app"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
// 上面部分都是自动生成的不用细看
dependencies {
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    // 使用 annotationProcessor 来指定作为 Annotation Processor 的模块
    annotationProcessor project(':compiler')
    // 引入自定义的注解
    implementation project(':annotation')
}

使用注解

package com.ajeyone.study.aptda;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import com.ajeyone.study.annotation.NiceField;

public class MainActivity extends AppCompatActivity {
    @NiceField // 👈👈👈 在这里
    int currentValue;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        currentValue = currentValue + 1;
    }
}

构建后生成了 HelloWorld.java:

总结

本文介绍了 APT 的原理以及自定义 APT 的流程和步骤。在研究过程中也发现了一些坑:

关于 APT 中另一个块重要的内容,如何通过注解获取源代码的信息,并生成目标源文件,建议阅读一些流行开源项目的源代码,比如 ButterKnife,Dagger2 等等。

另外还有一个比较新的东西是 incremental annotation processor,这个是 gradle 4.7 就搞出来的加快编译速度的功能,跟 java 本身没有关系,但是要引入 gradle 提供的一些工具来修改 Annotation Processor,感兴趣的可以参考一下官网的资料以及一些开源项目的实现,比如 Dagger2。https://docs.gradle.org/4.7/userguide/java_plugin.html#sec:incremental_annotation_processing

参考资料

  1. http://blog.chengyunfeng.com/?p=1021
  2. https://www.jianshu.com/p/9ca78aa4ab4d
  3. https://github.com/gradle/gradle/issues/5056
  4. https://blog.csdn.net/javazejian/article/details/71860633
  5. https://joyrun.github.io/2016/07/19/AptHelloWorld/
  6. http://www.voidcn.com/article/p-foftagul-uv.html
上一篇 下一篇

猜你喜欢

热点阅读