首页 > 代码库 > 自定义注解框架的那些事
自定义注解框架的那些事
一、前言
距离上次更新已过一个半月,工作太忙也不是停更的理由。我这方面做得很不好,希望大家给予监督。首先会讲解【编译期资源注入】,接着是【下拉刷新注入】(通过注解来实现下拉刷新功能),最后打造一款【特色的注解框架】。
大家准备好公交卡了吗,开车了 …
二、什么是注解
每位童鞋对 注解 都有自己的理解,字面上的意思就是【额外的加入】,在项目当中使用的注解的框架已经越来越多,如 : retrofit ,butterknife,androidannotations … 2017年Android百大框架排行榜 基本都会看到注解的身影 。
注解可以减少大量重复的工作,提高开发效率,注解也非常的灵活,可以注解到 类、方法、属性等 当中,用来自动完成一些规律性的代码,以及降低类与类之间的耦合度。
前一篇文章对运行时RUNTIME事件的注解做了一个简单的讲解 初谈Android-Annotations(二) , 本篇主要简单讲解编译时CLASS**res**文件下的资源注入。前者一般是通过反射来实现的,影响效率,接下来一起来了解下编译时CLASS的实现。
三、编译时的注解
类似 butterknife,androidannotations,arouter 使用的都是编译时的注解CLASS。这里以 butterknife 为例,来看一看以下注解:
/**
* Bind a field to the specified string resource ID.
* <pre><code>
* {@literal @}BindString(R.string.username_error) String usernameErrorText;
* </code></pre>
*/
@Retention(CLASS) @Target(FIELD) //编译时注解 针对属性(成员变量)注解
public @interface BindString {
/** String resource ID to which the field will be bound. */
@StringRes int value();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
如上定义了资源字符串的注解,那么怎么去实现字符串的注入的呢?
这里就不得不提到注解处理器 AbstractProcessor,注解处理器是 javac
的一个工具,它用来在编译时扫描和处理注解(Annotation)。以编译过的字节码作为输入,生成文件(一般是使用 javapoet 生成 .Java 文件)作为输出,生成的 Java 文件一样被 javac 编译。同样运行时注解 (RetentionPolicy.RUNTIME
) 和源码注解 (RetentionPolicy.SOURCE)
也可以在注解处理器进行处理。
注意:通过注解处理器处理注解是不能修改已经存在的 Java 类。例如增删一些方法。
四、字符串资源的注入
颜色(color),数组,尺寸的注入与字符串的注入类似,这里以字符串的注入来讲解。
先看看最终的实现效果:
log的打印日志:
通过日志可以看出:前面两个属性并没有在注解中引用字符串的资源,最终的结果和引用 R.string.app_name
的结果一样,这样可以节省代码量(为懒人服务)。目前支持驼峰式命名与前缀m
命名,规则由你改写,随性打造属于你自己的注入框架。
基础知识科普
如下所示,实现一个自定义注解处理器,至少重写四个方法,并且注册你的自定义Processor
。
- @AutoService(Processor.class),谷歌提供的自动注册注解,为你生成注册
Processor
所需要的格式文件。请在当前库的gradle
文件添加如下依赖:
dependencies {
compile fileTree(dir: ‘libs‘, include: [‘*.jar‘])
compile ‘com.google.auto.service:auto-service:1.0-rc3‘//添加
compile ‘com.google.auto:auto-common:0.8‘//添加
}
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
-
init(ProcessingEnvironment processingEnv) 初始化处理器,一般在这里获取我们需要的工具类
-
getSupportedAnnotationTypes() 返回注解器所支持的注解类型集合
-
getSupportedSourceVersion 返回
Java
版本 -
process 当于每个处理器的主函数
main()
,处理注解的逻辑(扫描、检验,以及生成Java文件),如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们,反之则要求后续 Processor 处理它们
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
private Types typeUtils; //类型工具
private Elements elementUtils;//元素工具
private Filer filer; //文件管理器
private Messager messager;//处理异常
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();//获取类的信息
elementUtils = processingEnv.getElementUtils();//获取程序的元素 如 包 类 方法
filer = processingEnv.getFiler();//生成java文件
messager = processingEnv.getMessager();//处理错误日志
}
/**
* @param annotations 请求处理的注解类型
* @param roundEnv 有关当前和以前的信息环境
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
/**
* @return 返回的是 注解类型的合法全称集合,如果没有这样的类型,则返回一个空集合
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<String>();
annotations.add(BindString.class.getCanonicalName());
return annotations;
}
/**
*
* @return 通常返回SourceVersion.latestSupported()
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
首先,我们梳理一下注解处理器的处理逻辑:
-
遍历备注解的元素
-
检验元素是否符合要求
-
获取输出类参数
-
生成 java 文件
-
错误处理
接着来看个简单示例获取被注解的元素名称,与注解值:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// roundEnv.getElementsAnnotatedWith()返回使用给定注解类型的元素
for (Element element : roundEnv.getElementsAnnotatedWith(BindString.class)) {
System.out.println("----------------------");
// 判断元素的类型为Class
if (element.getKind() == ElementKind.FIELD) {
// 显示转换元素类型
VariableElement variableElement= (VariableElement) element;
// 输出元素名称
System.out.println(""+variableElement.getSimpleName());
// 输出注解属性值
System.out.println(""+variableElement.getAnnotation(BindString.class).value());
}
System.out.println("----------------------");
}
return false;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
MainActivity 类:
@BindString(R.string.app_name)
String appName;
- 1
- 2
- 1
- 2
Gradle console 控制台输出如下:
看到这里,你一定会想,怎么样才能使 appName
与 2131099681
建立联系呢?
如果不使用注解是这样来建立联系的:
appName = getResources().getString(2131099681); // R.string.app_name = 2131099681
- 1
- 1
说一万道一千,实现这行代码的自动注入是我们的最终目的。
最后,我们来理解一下 Element
的概念,因为它是我们获取注解的基础。
Processor
处理过程中,会扫描全部的 Java 源码,代码的每一个部分都是一个特定类型的 Element
,它们像是XML
一层的层级机构,比如类、变量、方法等,每个Element
代表一个静态的、语言级别的构件。
Element
有五个直接子接口,它们分别代表一种特定类型的元素,如下:
-
PackageElement 表示一个包程序元素
-
TypeElement 表示一个类,接口或枚举程序元素 类型
-
VariableElement 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 属性
-
ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序方法(静态或实例),包括注解类型元素 方法
-
TypeParameterElement 表示一般类、接口、方法或构造方法元素的泛型参数 如
public class MainActivity<T>
泛型 T
在开发中Element
可根据实际情况强转为以上5种中的一种,它们都带有各自独有的方法,来看个简单的例子:
package com.github.wsannotation; // PackageElement
import java.util.List;
/**
* desc:
* author: wens
* date: 2017/8/11.
*/
public class UserBean // TypeElement
<T extends List> { // TypeParameterElement
private int age; // VariableElement
private String name; // VariableElement
public UserBean() { // ExecutableElement
}
public void setName(String name) { // ExecutableElement
String weight; // VariableElement
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
其中 Element
代表的是源代码,而 TypeElement
代表的是源代码中的类型元素,例如类。然而,TypeElement
并不包含类本身的信息。你可以从TypeElement
中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过TypeMirror
获取。你可以通过调用elements.asType()
获取元素的TypeMirror
。
相关文章链接:
自定义注解之编译时注解(RetentionPolicy.CLASS)(三)—— 常用接口介绍
字符串注入流程
这里参考了 ButterKnife
结合 androidannotations 的实现方式:
1、对元素效验 verify 系列方法
2、获取注解字段名和注解的资源ID以及处理资源ID为 -1 的情况
处理资源ID为 -1 的情况,我这里借鉴了 androidannotations 的处理方式:
//处理默认情况
if (resId == -1) {
TypeElement androidRType = elementUtils.getTypeElement("com.github.butterknifelib.R.string");
List<? extends Element> idEnclosedElements = androidRType.getEnclosedElements();
List<VariableElement> idFields = ElementFilter.fieldsIn(idEnclosedElements);
for (VariableElement idField : idFields) {
TypeKind fieldType = idField.asType().getKind();
if (fieldType.isPrimitive() && fieldType.equals(TypeKind.INT)) {
//制定规则
if (idField.getSimpleName().toString().toLowerCase().replaceAll("_", "")
.equals(name.startsWith("m") ? name.substring(1, name.length()).toLowerCase() : name.toLowerCase())) {
resId = (int) idField.getConstantValue();
break;
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
更好的方式是以 map
键值对来存储资源类 。后期我会做详细讲解。
3、生成 xxxx$$ViewBinder.java (xxxx 代码 Activity、View、Dialog)文件
4、给生成 xxxx$$ViewBinder.java 添加资源信息
5、通过 ButterKnife.bind(this);
反射得到 xxxx$$ViewBinder.java 类,并调用注入资源的方法
自定义注解框架的那些事