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

前言

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

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

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

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

img

APT的概念

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

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

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

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

使用场景举例

1.需求场景

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

Intent intent = new Intent(context, TestActivity.class);
intent.putExtra("id", id);
intent.putExtra("name", name);
intent.putExtra("is", is);
if (!(context instanceof Activity)) {intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivity(intent);

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

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

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

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

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

2.分析

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

(1)自动为TestActivity生成一个类,叫做TestActivityFastBundle; (2)提供构造者模式的链式调用,可以为需要的变量赋值; (3)提供一个build方法,可以返回一个Intent对象; (4)可以跳转到Activity,支持startActivity或startActivityForResult; (5)支持调用一个接口解析Intent中传递的数据,并赋值给Activity。

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

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

在TestActivity中,我们期望调用:

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

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

搭建APT项目

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

举例:

@Retention(CLASS)
@Target(FIELD)
public @interface AutoBundle {boolean require() default false;
}

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

implementation '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注册。

举例:

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

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

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment)

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

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

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

@Override
public Set<String> getSupportedAnnotationTypes()

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

4.在项目中引用

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

举例:

kapt project(':libProce')

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

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

img

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中的变量上设置注解:

@AutoBundle
public int id;
@AutoBundle
public String name;
@AutoBundle
public boolean is;

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

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

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

第一步:

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

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

第二步:

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

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

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

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

 // 获取类名String className = typeElement.getSimpleName().toString();// 获取包名String packageName = elements.getPackageOf(typeElement).getQualifiedName().toString();// 获取变量上的注解信息AutoBundle autoBundle = variableElement.getAnnotation(AutoBundle.class);boolean require = autoBundle.require();// 获取变量名
Name name = variableElement.getSimpleName();// 获取变量类型
TypeMirror type = variableElement.asType();

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

@AutoBundle(require = true)
public int id;

那么获取到数据后:

require = true
name = “id”
type = 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等。

img

示例代码:

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

2.MethodSpec.Builder

MethodSpec.Builder是方法的构建类。

img

示例代码:

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

3.FieldSpec.Builder

FieldSpec.Builder是变量的构建类。

img

示例代码:

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

4.JavaFile.Builder

img

示例代码:

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

5.各类Builder的方法

img

6.代码生成示例

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

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

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

MethodSpec.Builder buildMethodBuilder = MethodSpec.methodBuilder("build").addModifiers(Modifier.PUBLIC).returns(ClassName.get("android.content", "Intent"));buildMethodBuilder.addParameter(ClassName.get("android.content", "Context"), "context");buildMethodBuilder.addStatement(String.format("Intent intent = new Intent(context, %s.class)", classHolder.getClassName()));for (FieldHolder field : fields) {buildMethodBuilder.addStatement(String.format("intent.putExtra(\"%s\", %s)", field.getName(), field.getName()));
}buildMethodBuilder.addCode("if (!(context instanceof $T)) {\n", ClassName.get("android.app", "Activity"));buildMethodBuilder.addStatement("intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)");buildMethodBuilder.addCode("}\n");buildMethodBuilder.addStatement("return intent");contentBuilder.addMethod(buildMethodBuilder.build());
public Intent build(Context context) {Intent intent = new Intent(context, TestActivity.class);intent.putExtra("id", id);intent.putExtra("name", name);intent.putExtra("is", is);if (!(context instanceof Activity)) {intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);}return intent;
}

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

String fieldTypeName = field.getType().toString();3if (int.class.getName().equals(fieldTypeName)|| Integer.class.getName().equals(fieldTypeName)) {builder.addStatement(String.format("target.%s = intent.getIntExtra(\"%s\", 0)", field.getName(), field.getName()));} else if (String.class.getName().equals(fieldTypeName)) {builder.addStatement(String.format("target.%s = intent.getStringExtra(\"%s\")", field.getName(), field.getName()));} else if (boolean.class.getName().equals(fieldTypeName)|| Boolean.class.getName().equals(fieldTypeName)) {builder.addStatement(String.format("target.%s = intent.getBooleanExtra(\"%s\", false)", field.getName(), field.getName()));}
public void bind(TestActivity target, Intent intent) {target.id = intent.getIntExtra("id", 0);target.name = intent.getStringExtra("name");target.is = intent.getBooleanExtra("is", false);
}

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

JavaFile javaFile = JavaFile.builder(classHolder.getPackageName(), contentBuilder.build()).build();try {javaFile.writeTo(mFiler);
} catch (IOException e) {e.printStackTrace();
}

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

img

生成代码:

img

img

更多Android进阶指南 可以扫码 解锁 《Android十大板块文档》

1.Android车载应用开发系统学习指南(附项目实战)

2.Android Framework学习指南,助力成为系统级开发高手

3.2023最新Android中高级面试题汇总+解析,告别零offer

4.企业级Android音视频开发学习路线+项目实战(附源码)

5.Android Jetpack从入门到精通,构建高质量UI界面

6.Flutter技术解析与实战,跨平台首要之选

7.Kotlin从入门到实战,全方面提升架构基础

8.高级Android插件化与组件化(含实战教程和源码)

9.Android 性能优化实战+360°全方面性能调优

10.Android零基础入门到精通,高手进阶之路

敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/100654.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pytest + yaml 框架 -55. raw 不转义模板语法

前言 在yaml 文件中&#xff0c;设置的引用变量语法是${var}, 最近有小伙伴提到一个需求&#xff1a;请求参数的内容需要有特殊符号${var}, 希望不被转义&#xff0c;不要引用变量&#xff0c;直接用原始数据即可。 raw 忽略模板语法 Jinja2提供了 “raw” 语句来忽略所有模…

Gralloc ION DMABUF in Camera Display

目录 Background knowledge Introduction ia pa va and memory addressing Memory Addressing Page Frame Management Memory area management DMA IOVA and IOMMU Introduce DMABUF What is DMABUF DMABUF 关键概念 DMABUF APIS –The Exporter DMABUF APIS –The…

上海亚商投顾:沪指探底回升 华为汽车概念股集体大涨

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 三大指数昨日探底回升&#xff0c;早盘一度集体跌超1%&#xff0c;随后震荡回暖&#xff0c;深成指、创业板指…

交通物流模型 | 基于多重时空信息融合网络的城市网约车需求预测

交通物流模型 | 基于多重时空信息融合网络的城市网约车需求预测 城市网约车需求预测是网约车系统决策、出租车调度和智能交通建设的一项长期且具有挑战性的任务。准确的城市网约车需求预测可以提升车辆的利用和调度,减少等待时间和交通拥堵。现有的交通流预测方法大部分采用基…

LeetCode二叉树OJ

目录 剑指 Offer 55 - I. 二叉树的深度 - 力扣&#xff08;LeetCode&#xff09; 965. 单值二叉树 - 力扣&#xff08;LeetCode&#xff09; 100. 相同的树 - 力扣&#xff08;LeetCode&#xff09; 101. 对称二叉树 - 力扣&#xff08;LeetCode&#xff09; 二叉树遍历_牛客题…

Vue组件化开发步骤

Vue组件化开发的步骤可以简单概括为以下几步&#xff1a; 划分组件&#xff1a;根据页面的布局和功能需求&#xff0c;将页面划分成若干个组件&#xff0c;每个组件具备独立的功能和样式。 编写组件&#xff1a;针对每个组件&#xff0c;编写组件的模板、样式和逻辑代码&#…

uniapp 点击 富文本元素 图片 可以预览(非nvue)

我使用的是uniapp 官方推荐的组件 rich-text&#xff0c;一般我能用官方级用官方&#xff0c;更有保障一些。 一、整体逻辑 1. 定义一段html标签字符串&#xff0c;里面包含图片 2. 将字符串放入rich-text组件中&#xff0c;绑定点击事件itemclick 3. 通过点击事件获取到图片ur…

TensorFlow学习:使用官方模型进行图像分类、使用自己的数据对模型进行微调

前言 上一篇文章 TensorFlow案例学习&#xff1a;对服装图像进行分类 中我们跟随官方文档学习了如何进行预处理数据、构建模型、训练模型等。但是对于像我这样的业余玩家来说训练一个模型是非常困难的。所以为什么我们不站在巨人的肩膀上&#xff0c;使用已经训练好了的成熟模…

VIT(Vision Transformer)学习-模型理解(一)

VIT (Vision Transformer) 模型论文代码(源码)从零详细解读&#xff0c;看不懂来打我_哔哩哔哩_bilibili VIT模型架构图 1.图片切分为patch 2. patch转化为embedding 1&#xff09;将patch展平为一维长度 2&#xff09;token embedding&#xff1a;将拉平之后的序列映射…

【ARM AMBA5 CHI 入门 12.1 -- CHI 链路层详细介绍 】

文章目录 CHI 版本介绍1.1 CHI 链路层介绍1.1.1 Flit 切片介绍1.1.2 link layer credit(L-Credit)机制1.1.3 Channel1.1.4 Port1.1. RN Node 接口定义1.1.6 SN Node 接口定义1.2 Channel interface signals1.2.1 Request, REQ, channel1.2.2 Response, RSP, channel1.2.3 Snoop…

如何找到新媒体矩阵中存在的问题?

随着数字媒体的发展&#xff0c;企业的新媒体矩阵已成为品牌推广和营销的重要手段之一。 然而&#xff0c;很多企业在搭建新媒体矩阵的过程中&#xff0c;往往会忽略一些问题&#xff0c;导致矩阵发展存在潜在风险&#xff0c;影响整个矩阵运营效果。 因此&#xff0c;找到目前…

二维离散傅里叶变换的实现

二维离散傅里叶变换的实现 1.使用Python包实现1.1 fftshift在numpy中的实现1.2 平移后的幅度谱 2.使用c实现之12.1 FFTW库安装2.2 结果比较 3.使用c实现之2参考文献 1.使用Python包实现 import numpy as np import matplotlib.pyplot as plt anp.array([0, 2, 4, 1,6, 1, 3, …

快速幂求逆元

思路 题意&#xff1a; 给出两个整数 a , p a,p a,p&#xff0c;其中 p p p 是质数&#xff0c;求出一个整数 b b b&#xff0c;使得 a ∗ b 1 ( m o d p ) a~*~b~~1(mod~p) a ∗ b 1(mod p) 成立&#xff08;即求 a a a 模 p p p 的乘法逆元&#xff09;。 首先我们…

澳大利亚教育部宣布ChatGPT将被允许在澳学校使用!

教育部长最近宣布&#xff0c;从 2024 年起&#xff0c;包括 ChatGPT 在内的人工智能将被允许在所有澳大利亚学校使用。 &#xff08;图片来源&#xff1a;卫报&#xff09; 而早些时候&#xff0c;澳洲各高校就已经在寻找与Chatgpt之间的平衡了。 之前&#xff0c;悉尼大学就…

leetCode 1035.不相交的线 动态规划 + 滚动数组 (最长公共子序列)

1035. 不相交的线 - 力扣&#xff08;LeetCode&#xff09; 在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在&#xff0c;可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线&#xff0c;这些直线需要同时满足满足&#xff1a; nums1[i] nums2[j]…

vue3+elementplus+flask 文件夹上传

<div><input class"fileuploadclass" title"请选择文件夹" ref"file" id"submit" type"file"multiple name"" webkitdirectory change"folderModelOn"><el-button type"primary&qu…

冒泡排序/鸡尾酒排序

冒泡排序 冒泡排序&#xff08;Bubble Sort&#xff09;是一种简单的排序算法&#xff0c;它通过多次交换相邻元素的位置来实现排序。它的基本思想是从数组的第一个元素开始&#xff0c;比较相邻的两个元素&#xff0c;如果它们的顺序错误&#xff0c;则交换它们的位置。重复进…

蓝桥杯每日一题20233.10.10

题目描述 回文日期 - 蓝桥云课 (lanqiao.cn) 题目分析 对于此题&#xff0c;我们最先想到的是暴力解法&#xff0c;将每一种情况经行循环查找&#xff0c;在查找的过程中记录下答案&#xff0c;回文日期就是字符串判断回文&#xff0c;ABABBABA型回文日期可以将回文经行特判…

JVM源码剖析之Thread类中sleep方法

版本信息&#xff1a; jdk版本&#xff1a;jdk8u40 写在前面&#xff1a; 大部分的Java程序员知道让线程睡眠的方法是Thread.sleep方法&#xff0c;而这个方法是一个native方法&#xff0c;让很多想知道底层如何让线程睡眠的程序员望而却步。所以笔者特意写在这篇文章&#xf…

openGauss学习笔记-96 openGauss 数据库管理-访问外部数据库-file_fdw

文章目录 openGauss学习笔记-96 openGauss 数据库管理-访问外部数据库-file_fdw96.1 使用file_fdw96.2 注意事项 openGauss学习笔记-96 openGauss 数据库管理-访问外部数据库-file_fdw openGauss的fdw实现的功能是各个openGauss数据库及远程服务器&#xff08;包括数据库、文件…