基本概念
APT 全称为 Annotation Processing Tool,可翻译为注解处理器,APT 工具是用于注解处理的命令行程序,它可以找到源码中对应注解的对象并使用注解处理器对其进行处理。
一般来说,我们会使用 APT 生成一些源码,然后加入编译目录进行编译,从而简化开发周期。
注解
注解处理器是基于注解(Annotation)的,实际开发中自定义注解用的比较少,这里先简单的复习下相关概念。
Java 注解是 Java1.5 中引入的概念,是用来标注代码的元数据。
定义一个注解
定义一个注解使用 @interface 关键字。
public @interface Test {}
对,这样一个名为 Test 的注解就定义好了。
元注解
元注解是注解的注解,用来标注注解的元数据。
可以通过元注解来控制注解的一些属性与行为。
Java 中元注解共有四种:Retention、Inherited、Documented、Target。
Retention
Retention 表示注解的保留范围,取值为枚举类:RetentionPolicy,其中一共包括如下三个类型。
- SOURCE:注解只保留在源文件中,编译时就会把注解丢弃。
- CLASS:默认值,注解保留到 class 文件中,运行时会丢弃。
- RUNTIME:注解将会一直保留到运行时,可以通过反射获取到该注解。
Target
Target 用于控制注解的使用范围,取值为枚举类:ElementType。
- TYPE:该注解作用于类,接口或枚举类上
- FIELD:作用于类属性或对象属性也可以为枚举实例
- METHOD:作用于函数
- PARAMETER:作用于函数参数
- CONSTRUCTOR:作用于构造器
- PACKAGE:作用于包名
- LOCAL_VARIABLE:作用于变量
- ANNOTATION_TYPE:作用于注解
- TYPE_PARAMETER:Java 1.8 引入,作用于类型参数例如:
@Target(ElementType.TYPE_PARAMETER)
public @interface Test {}
class TypeTest<@Test T>{}
- TYPE_USE:Java 1.8 引入,作用于各种类型,也就是说,任何使用类型的地方都可以使用该注解。
@Target(ElementType.TYPE_USE)
public @interface Test {}
public class A {}
public class B extends @Test A {
public @Test int add(@Test int a, @Test int b) {
@Test int result = a + b;
System.out.println(result);
return result;
}
}
Documented
Documented 用于描述该注解是否需要加入到例如 javadoc 工具生成的文档的公共 API 中,使用 Documented 的注解将会被保留在生成的文档中。
Inherited
使用 Inherited 元注解的注解修饰的类,子类将会继承这个注解,这么说有点绕,举个栗子。
首先定义注解 Test,且使用 @Inherited 元注解修饰。
然后定义一个 Father 类,使用 @Test 注解修饰,再定义一个 Son 类继承了 Father 类,那么 Son 也会拥有注解 Test。
OK,注解就说完了,回归正题,开始介绍 APT。
APT
APT 的原理就是在需要使用的元素上(类、变量、方法、参数等)加上我们的注解,然后在编译时把使用了这个注解的元素都收集起来,做统一的处理,例如根据元素生成对应的工具类,以此提高开发效率。
创建一个注解处理器分为如下几步:
- 创建注解类
- 创建一个继承自 AbstractProcessor 的类,这就是 APT 的核心类
- 创建公开 API 及辅助工具
- 创建配置文件
- 使用
做 Android 开发的应该都知道 JakeWharton 大神的 ButterKnife 框架,这个框架就是通过 APT 技术实现的,不知道的也没关系,这个框架的功能就是通过注解给对象赋值。我们一般创建控件后还需要使用控件 ID 来获取控件的对象,控件多了之后比较麻烦,很多重复代码,这个框架就是直接通过控件 ID 来初始化控件的,我现在来模仿一下做个山寨版的 CopycatKnife 学习 APT。
原理就是我们根据注解所在的类生成一个对应的工具类,在其中提供 bind 和 unbind 方法,在这两个方法中来绑定 View 以及解除绑定。
我们现在新建个工程命名为:CopycatKnife。
创建注解类
先创建一个 Java Library 的子模块专门用于存放注解类。我这里将它命名为 annotaions。
毕竟是为了学习 APT,这里就只完成 ButterKnife 的一个 BindView 功能就好了,我们先来定义一个 BindView 注解。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
int value();
}
因为 BindView 注解是用在属性上的,所以 Target 设置为 Field,然后我们只需要在编译期获取注解并生成代码,所以指定 Retention 为 SOURCE。
BindView 是需要一个 View 的 id 作为参数的,所以这里提供一个返回类型为 int 的方法接收这个参数,这也是注解参数的定义方式,同样可以定义多个参数。
AbstractProcessor
注解处理器需要单独创建一个 Java Library 子模块来存放,我们创建一个名为 complier 的子模块。然后创建一个 BindViewProcessor 类实现 AbstractProcessor 抽象类。这就是我们的注解处理器。
AbstractProcessor 就是 APT 的核心类了,每个 AbstractProcessor 的子类都是一个注解处理器,我们通过继承并实现这个类来完成注解处理器的功能。
该类主要有一下几个方法需要我们实现。
getSupportedAnnotationTypes()
这个方法将会返回一个该注解处理器支持的注解类型。我们目前只支持 BindView 注解,那可以这么写:
override fun getSupportedAnnotationTypes(): Set<String> {
//返回该处理器可以处理的注解集合
return setOf(BindView::class.java.canonicalName)
}
我们返回了一个只包含 BindView 注解的 Set 集合。
process
这个方法是关键,真正处理解析注解元素并生成 Java 代码的就是这个方法。
我们看下这个方法的签名:
public abstract boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv);
第一个参数 annotations 就是当前注解处理器支持的注解集合,第二个 roundEnv 表示当前的 APT 环境,其中提供了很多API,可以通过它获取到注解元素的一些信息。其中最重要的就是 getElementsAnnotatedWith 方法,通过它可以获取到所有使用了该注解的元素。
//获取所有使用了 BindView 注解的元素
val bindViewElementSet = roundEnv.getElementsAnnotatedWith(BindView::class.java)
此时已经获取到了所有使用 BindView 注解的元素。
CopycatKnife 的功能是通过注解直接初始化 View,我这里的方案跟 ButterKnife 一样,都是根据每个使用了 BindView 元素对应的类生成一个绑定类,例如在 MainActivity.java 中有几个使用了 BindView 注解的元素,那么我们就生成一个名为 MainActivity_Binding.java 的类。
而一个项目可能有多个类里面的 View 使用了 BindView 注解,所以第一步应该根据类对元素进行分组。
private fun groupingElementWithType(eleSet: Set<Element>): Map<TypeElement, ArrayList<Element>> {
val groupedElement = HashMap<TypeElement, ArrayList<Element>>()
for (item in eleSet) {
checkAnnotationLegal(item)
val enclosingElement = item.enclosingElement as TypeElement
if (groupedElement.keys.contains(enclosingElement)) {
groupedElement[enclosingElement]!!.add(item)
} else {
val list = ArrayList<Element>()
list += item
groupedElement[enclosingElement] = list
}
}
return groupedElement
}
在分组前,为了保证元素的合法性,我们先对其进行校验,防止出现错误:
private fun checkAnnotationLegal(ele: Element) {
if (ele.kind != ElementKind.FIELD) {
throw RuntimeException("@BindView must in filed! $ele kind is ${ele.kind}")
}
val modifier = ele.modifiers
if (modifier.contains(Modifier.FINAL)) {
throw RuntimeException("@BindView filed can not be final!")
}
if (modifier.contains(Modifier.PRIVATE)) {
throw RuntimeException("@BindView filed can not be private")
}
}
校验完成后我们对其进行下一步的操作。
此时需要做的就是解析元素并生成类和方法了,生成 Java 文件这里使用 javapoet 框架:
implementation "com.squareup:javapoet:1.12.1"
关于这个框架的使用方法请看:
https://github.com/square/javapoet
我们现在先创建构造器:
private fun makeConstructor(typeElement: TypeElement): MethodSpec.Builder {
val typeMirror = typeElement.asType()
return MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.get(typeMirror), "target")
}
如果使用 BindView 注解的元素在 MainActivity 中的话,我们通过上述代码将会生成如下的一个空的构造器:
public MainActivity_Binding(MainActivity target) {}
下面就是实现这个构造器了,也就是在其中对使用了 BindView 注解的元素进行赋值:
for (itemView in elements) {
bindMethodBuilder.addStatement("target.${itemView} = " +
"target.findViewById(${itemView.getAnnotation(BindView::class.java).value})")
}
上面会生成类似下面的 Java 代码如下:
target.tvMain01 = target.findViewById(2131165359);
到了这一步主要功能就完成了, 元素已经全部被初始化了。
下面我们再来生成类并把这个方法加进去:
val typeBuilder = TypeSpec.classBuilder("${typeEle.simpleName}_Binding")
.addModifiers(Modifier.PUBLIC)
typeBuilder.addMethod(constructor)
//生成一个 Java 类文件
val file = JavaFile.builder(getPackageName(classItem), typeBuilder.build())
.build()
file.writeTo(this.processingEnv.filer)
这样,一个具备绑定及解除绑定元素的完整的类就输出完成了。
创建公开 API 及辅助工具
现在我们还差一个步骤既可完成,我们知道,在使用 BindView 注解时不单单是在元素上使用注解,还需要在 Activity#onCreate 方法中调用 bind 方法绑定才行,然后在 bind 方法中创建我们刚刚通过 APT 生成的 ViewBinding 类,那么我们现在就需要提供一个 bind 方法以及在其中通过反射创建 ViewBinding 类。
所以我们还需要再创建一个 Java Library,然后创建一个 CopycatKnife 类,里面提供一个 bind 方法。
fun bind(activity: Activity) {
val targetClass = activity::class.java
val constructor = findBindingConstructorForClass(targetClass)
constructor?.newInstance(activity)
}
private fun findBindingConstructorForClass(cls: Class<*>?): Constructor<*>? {
if (cls == null) return null
var bindingConstructor: Constructor<*>? = null
val clsName = cls.name
try {
val bindingClass = cls.classLoader!!.loadClass(clsName + "_ViewBinding")
bindingConstructor = bindingClass.getConstructor(cls)
} catch (e: ClassNotFoundException) {
bindingConstructor = findBindingConstructorForClass(cls.superclass)
} catch (e: NoSuchMethodException) {
throw RuntimeException("Unable to find binding constructor for $clsName", e)
}
return bindingConstructor
}
反正这个也不属于 APT 范畴,就不详细介绍了,而且也是仿照(抄袭)ButterKnife 的。
通过上面几个步骤,我们的 CopycatKnife 就基本完成了。
创建配置文件
现在再来个简单的配置就行了,我们需要声明一下各个创建的 APT,现在回到 complier 模块中,先在 main 目录下创建配置文件目录,路径为:
main\resources\META-INF\services
然后在其中创建一个 名为 javax.annotation.processing.Processor 的配置文件,将刚刚创建的 BindViewProcessor 全限定名称加入其中:
com.zhangke.complier.BindViewProcessor
好了,配置代码就这么多。
使用
现在已经全部搞定了,使用就很简单了,配置好依赖后按照 ButterKnife 的使用方式一样使用即可。
全部代码包括使用案例已经放在了我的 Github 上,点击下面的连接查看:
https://github.com/0xZhangKe/CopycatKnife
好了,简陋山寨抄袭版的 ButterKnife 就完成啦,关于 APT 技术的使用就介绍到这了,欢迎关注我的公众号,还有更多干货。
如果觉得还不错的话,欢迎关注我的个人公众号:zhangke_blog