如何在Android中完成一个APT项目的开发?
☝点击上方蓝字,关注我们!
本文字数: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 (CLASS)
2 (FIELD)
3public 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
2public synchronized void init(ProcessingEnvironment processingEnvironment)
3
Processor的初始化方法,在编译阶段会首先回调此方法,ProcessingEnvironment类包含了解析需要的数据对象,我们可以通过它获取到一系列我们需要的其他对象,进而获取到需要的数据。
1
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
2public int id;
3
4public String name;
5
6public boolean is;
其中AutoBundle注解是我们自己定义的注解类。
初步设计好后,我们需要在process方法中重写我们的逻辑:
1
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 (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指令编译)。
生成代码:
-❼-
开发流程总结
狐友技术团队其他精彩文章
分布式追踪系统概述及主流开源系统对比
安卓系统权限,你真的了解吗?
Swift之Codable实战技巧
AspectJ在Android中的应用
加入搜狐技术作者天团
千元稿费等你来!
戳这里!☛