18

如何在 Android 中完成一个 APT 项目的开发?

 5 years ago
source link: https://www.tuicool.com/articles/eQnQVrb
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

E7zAr2Z.gif

本文字数: 3790

预计阅读时间: 25分钟

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具。

APT在编译时期扫描处理源代码中的注解,开发中可以根据注解,利用APT自动生成Java代码,减少冗余的代码和手动的代码输入过程,提升了编码效率,同时使源代码看起来更清晰简洁,可读性提升。

目前,很多第三方开源框架采用APT技术,以减少开发者的重复工作。常见的如ButterKnife、EventBus等。

本文侧重于实际应用的讲解,以Android APP开发过程中一个常见的页面跳转场景为示例,从搭建项目、APT数据与功能介绍、提取数据和自动化生成代码几个过程,逐步讲解如何完成一个APT项目的开发。

目录:

1

APT的概念

简单介绍APT的概念,以及目前的应用情况;

2

使用场景举例

通过引入一个实际场景,逐步进行讲解;

3

搭建APT项目

如何在Android Studio搭建一个APT项目;

4

APT中的数据类型与概念

简单介绍APT中常见的数据类型;

5

APT处理过程拆解

拆解APT process过程,如何获取注解所需数据;

6

JavaPoet自动化代码生成

如何根据获取到的注解数据,生成.java代码文件;

7

开发流程总结

整体流程示意图。

-❶-

APT的概念

APT即注解处理器(Annotation Processor Tool),是javac内置的一个用于编译时扫描和处理注解的工具。简单的说,在源代码编译阶段,通过注解处理器,我们可以获取源文件内注解相关内容。

由于注解处理器可以在程序编译阶段工作,所以我们可以在编译期间通过注解处理器进行我们需要的操作。比较常用的用法就是在编译期间获取相关注解数据,然后动态生成代码源文件。

通常注解处理器是用于自动产生一些有规律性的重复代码,解决了手工编写重复代码的问题,大大提升编码效率。

目前很多比较著名的开源框架使用了此技术,如ButterKnife为开发人员解决了手动编写大量findViewById方法的问题。其它如GreenDao中使用的JDT与APT思想完全一致,只是IDE与工具不同。

-❷-

使用场景举例

1.需求场景

在Android开发中,Activity的跳转是必不可少的操作。当需要通过Intent传递数据的时候,代码一般是如下所示:

1Intent intent = new Intent(context, TestActivity.class);
2intent.putExtra("id", id);
3intent.putExtra("name", name);
4intent.putExtra("is", is);
5if (!(context instanceof Activity)) {
6    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
7}
8startActivity(intent);

以及在TestActivity中的获取数据操作:

1id = intent.getIntExtra("id", 0);
2name = intent.getStringExtra("name");
3is = intent.getBooleanExtra("is", false);

以上代码的问题在于,对于每个过程都需要编写类似的代码,重复性比较大,浪费时间。

在数据传递和解析时,key需要保持一致。我们可以用常量来代替,但将定义很多常量。

我们希望以上代码可以自动化生成,开发者只需要调用几个可读性更好的方法,即可实现上述过程。

2.分析

针对这个需求场景,我们需要实现的自动化功能如下:

(1)自动为TestActivity生成一个类,叫做TestActivityFastBundle;

(2)提供构造者模式的链式调用,可以为需要的变量赋值;

(3)提供一个build方法,可以返回一个Intent对象;

(4)可以跳转到Activity,支持startActivity或startActivityForResult;

(5)支持调用一个接口解析Intent中传递的数据,并赋值给Activity。

我们期望简化后调用时候是这样的,这将跳转到TestActivity:

1new TestActivityFastBundle()
2        .id(1)
3        .is(true)
4        .name("user")
5        .launch(this); // 或者使用launchForResult

在TestActivity中,我们期望调用:

1new TestActivityFastBundle().bind(this, getIntent());

实现自动将Intent中的变量赋值给当前类中的变量。

-❸-

搭建APT项目

1.创建一个Android Library,并创建自己需要的注解类。

举例:

1@Retention(CLASS)
2@Target(FIELD)
3public @interface AutoBundle {
4    boolean require() default false;
5}

2.创建一个Java Library,引用步骤1中所创建的Android Library,并为这个Java Library添加依赖。

1implementation 'com.google.auto.service:auto-service:1.0-rc2'

介绍一下这个库是做什么用的:

因为注解处理器是在编译期间进行工作,需要向编译器进行“注册”,让编译器知道需要使用哪个注解器处理数据。

如果不使用auto-service库,那么手动注册的方法如下:

1.在Library中创建resources文件夹;

2.在resources中创建META-INF和services两个文件夹;

3.在services中创建一个文件,命名为javax.annotation.processing.Processor;

4.在javax.annotation.processing.Processor文件中输入自己所创建的注解处理器类名(完整的,包括包名)。

3.创建自己的处理类,继承AbstractProcessor,并使用auto-service注册。

举例:

1@AutoService(Processor.class)
2public class AutoBundleProcessor extends AbstractProcessor

在创建AbstractProcessor子类后,我们需要重写其中的几个方法,来实现自己的处理逻辑:

1@Override
2public synchronized void init(ProcessingEnvironment processingEnvironment)
3

Processor的初始化方法,在编译阶段会首先回调此方法,ProcessingEnvironment类包含了解析需要的数据对象,我们可以通过它获取到一系列我们需要的其他对象,进而获取到需要的数据。

1@Override
2public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

process方法在编译过程中回调,在此我们可以获取到我们需要的类、对象及其对应的注解,在此可以分析并处理数据,最终生成我们需要的代码。

1@Override
2public Set<String> getSupportedAnnotationTypes()

getSupportedAnnotationTypes方法帮助我们获得所需要的注解类。我们将自己需要的类名放入Set中并返回给注解处理器,换句话说,在这里为注解处理器指定需要处理哪些注解。

4.在项目中引用

在主项目的gradle中引用包含注解的Android Library引用注解器所在的Java Library。由于kotlin的引入,建议使用kapt而非annotationProcessor。

举例:

1kapt project(':libProce')

至此,工程整体结构已经搭建完成。

后续将介绍APT中各种类和对象的作用,以及如何实现我们需要的功能。

-❹-

APT中的数据类型与概念

1.ProcessingEnvironment

当我们在子类中复写了AbstractProcessor的init方法时,其参数就是一个ProcessingEnvironment对象。它内部提供了实用的对象,如Elements、Types、Filer,在APT过程中都具有重要作用。我们可以获取到这些对象,来实现我们需要的功能。

2.Element

在APT阶段,任何事物都被称为元素。比如一个对象、一个类、一个方法、一个参数。在APT中,它们都被统一称为元素。Element本身是一个接口,也有多个子类,比如TypeElement、VariableElement,子类在其基础上增加了额外的接口方法来描述具体事物的特殊属性。

3.ElementKind

由于在APT中,任何事物都被称为元素,所以我们需要知道某个元素究竟是什么,这时候可以通过ElementKind判断。

ElementKind是一个枚举类。其中包括但不限于PACKAGE(包)、CLASS(类)、INTERFACE(接口)、FIELD(变量)、PARAMETER(参数)、METHOD(方法)等。这些都是我们开发中的基本概念。

4.Elements

Elements可以理解为一个工具类,它的功能就是操作Element对象,对Element对象进行一些处理或取值。

5.TypeElement

TypeElement是Element子类,它表示这个元素是一个类或者接口。当Element满足条件时候,可以强转为一个TypeElement对象。

6.VariableElement

VariableElement是Element子类,它表示这个元素是一个变量、常量、方法、构造器、参数等。当Element满足条件时候,可以强转为一个VariableElement对象。

7.Filer

Filer是一个文件操作的接口,它可以创建或写入一个Java文件。主要针对的是Java文件对象,和一般文件的区别在于这是专门处理Java类文件的,以.java或.class为后缀的文件。在APT过程中,如果我们自动化代码生成完毕,需要生成一个.java或.class文件的时候,就需要用到Filer。

8.Name

Name类是CharSequence的子类,主要表示类名、方法名。大部分情况下可以认为它和String等价。

9.Types

Types可以理解为一个工具类,是类型操作工具,在APT阶段,我们需要知道一个变量是int还是boolean,那将需要通过Types相关类处理。它可以操作TypeMirror对象。

10.TypeMirror

TypeMirror表示数据类型。比如基本类型int、boolean,也可以表示复杂数据类型,比如自定义类、数组、Parcelable等。

11.Modifier

即修饰词。比如声明一个变量时候,private static final这些均为修饰词。大部分被Android Studio标示为蓝色的都是修饰词(除了class int interface这些)。

注:如果一个类中的变量缺省作用范围,那么修饰词为default。

12.RoundEnvironment

当我们在子类中复写了AbstractProcessor的process方法时,其参数就是一个RoundEnvironment对象。可以通过RoundEnvironment对象获取到我们在代码中设置了相关注解的Element。

-❺-

APT处理过程拆解

下面将以上文中所举出的场景,逐步对APT处理过程进行拆解,最终获取到我们需要的属性,为生成自动化代码做准备。

在TestActivity中的变量上设置注解:

1@AutoBundle
2public int id;
3@AutoBundle
4public String name;
5@AutoBundle
6public boolean is;

其中AutoBundle注解是我们自己定义的注解类。

初步设计好后,我们需要在process方法中重写我们的逻辑:

1@Override
2public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

第一步:

获取所有被AutoBundle注解所声明的元素。这里我们知道,其实只有三个变量;

1for (Element element : roundEnvironment.getElementsAnnotatedWith(AutoBundle.class)) {
2
3}

第二步:

对每个循环中的Element对象,获取其数据信息;

1if (element.getKind() == ElementKind.FIELD) {
2    // 可以安全地进行强转,将Element对象转换为一个VariableElement对象
3    VariableElement variableElement = (VariableElement) element; 
4    // 获取变量所在类的信息TypeElement对象
5    TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();

variableElement中包含的数据包括修饰词、类型、变量名等;

typeElement中包含的数据包括类名、包名等。

 1// 获取类名
 2String className = typeElement.getSimpleName().toString();
 3
 4// 获取包名
 5String packageName = elements.getPackageOf(typeElement).getQualifiedName().toString();
 6
 7// 获取变量上的注解信息
 8AutoBundle autoBundle = variableElement.getAnnotation(AutoBundle.class);
 9boolean require = autoBundle.require();
10
11// 获取变量名
12Name name = variableElement.getSimpleName();
13
14// 获取变量类型
15TypeMirror type = variableElement.asType();

对于我们上文定义的某个变量,比如:

1@AutoBundle(require = true)
2public int id;

那么获取到数据后:

1require = true
2name = “id”
3type = int.class

其他两个变量同理。

三次循环将获取到我们需要的所有信息。

包括三个变量的注解值、变量名、类型。同时我们也获取到了TestActivity的类名和包名。可以对这些数据进行一些封装和缓存。接下来就可以自动化生成代码了。

我将上述变量值封装为ClassHoder与FieldHolder类中,ClassHolder保存了类名、包名等信息,FieldHolder保存了每个变量类型、变量名、注解等信息。下面将用这些保存好的数据,通过JavaPoet生成代码。

-❻-

JavaPoet代码自动化生成

JavaPoet是Java代码自动生成框架,是一个github上的开源项目,地址:https://github.com/square/javapoet 。

JavaPoet简化了Java代码生成的开发难度,通过建造者模式,使调用更加人性化,可读性提升。具有自动import的功能,不需要再手动指定。

JavaPoet中,大部分数据类型使用了APT中通用的类型,结合APT自动化产生代码非常方便快速。

1.TypeSpec.Builder

TypeSpec.Builder是类的构建类,这里的类是广义上的,可以是一个class、interface、annotation等。

方法

功能

classBuilder

annotationBuilder

注解

interfaceBuilder

接口

anonymousClassBuilder

匿名类

示例代码:

1TypeSpec.Builder contentBuilder = TypeSpec.classBuilder("yourClassName")

2.MethodSpec.Builder

MethodSpec.Builder是方法的构建类。

方法

功能

constructorBuilder

构造方法

methodBuilder

方法

示例代码:

1MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("yourMethodName")

3.FieldSpec.Builder

FieldSpec.Builder是变量的构建类。

方法

功能

builder

创建一个变量

示例代码:

1FieldSpec.Builder fieldBuilder = FieldSpec.builder(ClassName.get(field.getType()), "yourFieldName", Modifier.PRIVATE)

4.JavaFile.Builder

方法

功能

builder

创建一个JavaFile对象

writeTo

将数据写成Java文件,支持Filer对象、路径等多种方式

示例代码:

1JavaFile javaFile = JavaFile.builder(classHolder.getPackageName(), contentBuilder.build())
2javaFile.writeTo(mFiler);

5.各类Builder的方法

方法

功能

描述

addModifier

添加修饰词

Modifier对应Java中的Modifier类,类中的变量均可用。比如private、public、protected、static、final等。

示例:

addModifiers(Modifier.PUBLIC)

addParameter

添加参数

方法中的参数。

示例:

addParameter(ClassName.get("android.content",  "Intent"), "intent")

addParameter(int.class, "requestCode")

addStatement

添加描述

直接添加代码语句。

示例:

addStatement("return this")

addCode

添加代码文字

直接添加代码语句。与addStatement的区别在于,addStatement仅按纯文本处理,而addCode按照代码语言处理。

同时addCode会自动帮你import其中使用到的类,并在语句末尾添加分号。

示例:

addCode("if (!(context instanceof $T)) {\n",  ClassName.get("android.app", "Activity"));

returns

添加返回值

为方法添加返回值。

示例:

returns(void.class);   

returns(ClassName.get("android.content", "Intent"));

addMethod

添加方法

将一个构造好的方法对象添加到类中。

示例:

addMethod(methodBuilder.build());

addField

添加变量

将一个变量添加到类中。

示例:

addField(fieldBuilder.build());

6.代码生成示例

构造代码与生成结果示例1:

1for (FieldHolder field : fields) {
2    FieldSpec f = FieldSpec.builder(ClassName.get(field.getType()), field.getName(), Modifier.PRIVATE)
3            .build();
4contentBuilder.addField(f);
1private int id;
2private String name;
3private boolean is;

构造代码与生成结果示例2:

 1MethodSpec.Builder buildMethodBuilder = MethodSpec.methodBuilder("build")
 2        .addModifiers(Modifier.PUBLIC)
 3        .returns(ClassName.get("android.content", "Intent"));
 4
 5buildMethodBuilder.addParameter(ClassName.get("android.content", "Context"), "context");
 6
 7buildMethodBuilder.addStatement(String.format("Intent intent = new Intent(context, %s.class)", classHolder.getClassName()));
 8
 9for (FieldHolder field : fields) {
10    buildMethodBuilder.addStatement(String.format("intent.putExtra(\"%s\", %s)", field.getName(), field.getName()));
11}
12
13buildMethodBuilder.addCode("if (!(context instanceof $T)) {\n", ClassName.get("android.app", "Activity"));
14
15buildMethodBuilder.addStatement("intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)");
16
17buildMethodBuilder.addCode("}\n");
18
19buildMethodBuilder.addStatement("return intent");
20
21contentBuilder.addMethod(buildMethodBuilder.build());
 1public Intent build(Context context) {
 2  Intent intent = new Intent(context, TestActivity.class);
 3  intent.putExtra("id", id);
 4  intent.putExtra("name", name);
 5  intent.putExtra("is", is);
 6  if (!(context instanceof Activity)) {
 7  intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 8  }
 9  return intent;
10}

构造代码与生成结果示例3:

 1String fieldTypeName = field.getType().toString();
 2
 3if (int.class.getName().equals(fieldTypeName)
 4        || Integer.class.getName().equals(fieldTypeName)) {
 5
 6    builder.addStatement(String.format("target.%s = intent.getIntExtra(\"%s\", 0)", field.getName(), field.getName()));
 7
 8} else if (String.class.getName().equals(fieldTypeName)) {
 9
10    builder.addStatement(String.format("target.%s = intent.getStringExtra(\"%s\")", field.getName(), field.getName()));
11
12} else if (boolean.class.getName().equals(fieldTypeName)
13        || Boolean.class.getName().equals(fieldTypeName)) {
14
15    builder.addStatement(String.format("target.%s = intent.getBooleanExtra(\"%s\", false)", field.getName(), field.getName()));
16
17}
1public void bind(TestActivity target, Intent intent) {
2  target.id = intent.getIntExtra("id", 0);
3  target.name = intent.getStringExtra("name");
4  target.is = intent.getBooleanExtra("is", false);
5}

7.将生成好的代码写入文件

1JavaFile javaFile = JavaFile.builder(classHolder.getPackageName(), contentBuilder.build()).build();
2
3try {
4    javaFile.writeTo(mFiler);
5} catch (IOException e) {
6    e.printStackTrace();
7}

构建一个JavaFile对象,将构造好的TypeSpecBuilder内容放入,并写入到Filer中即可。编译后此类文件便生成在对应包下,如图所示,自动生成文件在build/generated/source/kapt下(使用kapt指令编译)。

VBnArqv.jpg!web

生成代码:

EBjuEbv.jpg!web

YbiuQzm.jpg!web

-❼-

开发流程总结

aY73eaF.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK