上一篇我们讲解了ButterKnife的设计思想,理解了ButterKnife绑定相关源码的实现逻辑。但是它是怎么通过注解的方式生成的那些逻辑代码,这才是最让我们迫切想知道,因此在这篇,我将说说ButterKnife中注解处理的原理。本篇主要有以下内容:
- 注解Annotation
- 注解处理器AbstractProcessor
- AutoService注册注解处理器
- JavaPoet生成Java代码
- Element元素相关
- 编译时注解解析
- 例子项目
注解(Annotation)
注解(Annotation)在Java中已经是很普遍的使用了,它其实就是一种标记信息,然后程序在编译或者运行的时候可以读取这个标记信息,去执行特定的逻辑,比如@BindView(R.id.tv_text) TextView tvText,程序在编译时会读取到这个@BindView注解,解析出它的值R.id.tv_text,再根据它注解的这个tvText,就可以生成类似tvText = (TextView)findViewById(R.id.tv_text);的功能代码。
注解按生命周期可以分为
- RetentionPolicy.SOURCE(源码注解),只在源码中存在,在编译时会被丢弃,通常用于检查性的操作,如@Override。
- RetentionPolicy.CLASS(编译时注解),在编译后的class文件中依然存在,通常用于编译时处理,如ButterKnife的@BindView。
- RetentionPolicy.RUNTIME(运行时注解),不仅在编译后的class文件中存在,在被jvm虚拟机加载之后,仍然存在,通常用于运行时处理,如Retrofit的@Get。
同时注解按使用的对象可以分为
- ElementType.TYPE(类型注解),标记在接口、类、枚举上。
- ElementType.FIELD(属性注解),标记在属性字段上。
- ElementType.METHOD(方法注解),标记在方法上。
- ElementType.PARAMETER(方法参数注解),标记在方法参数上。
- ElementType.CONSTRUCTOR(构造方法注解),标记在构造方法上。
- ElementType.LOCAL_VARIABLE(本地变量注解),标记在本地变量上。
- ElementType.ANNOTATION_TYPE(注解的注解),标记在注解上。
- ElementType.PACKAGE(包注解),标记在包上。
- ElementType.TYPE_PARAMETER(类型参数注解,Java1.8加入),标记类型参数上。
- ElementType.TYPE_USE(类型使用注解,Java1.8加入),标记在类的使用上。
我们首先来认识ButterKnife的一个自定义属性注解@BindView
/**
* 作用于View的注解,如@BindView(R.id.text) TextView tvText
*
* @Retention(RetentionPolicy.CLASS) 表示生命周期到类的编译时期
* @Target(ElementType.FIELD) 表示注解作用在字段上
*
* Created by Administrator on 2017/12/31 0031.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
@android.support.annotation.IdRes
int value();
}
它可以保留在类编译之后,使用场景是作用在属性上
关于注解的详细介绍查看Java注解基础概念总结
注解处理器AbstractProcessor
注解只是一种标记信息,所以需要我们自己去处理注解,注解的处理有编译时注解处理和运行时注解处理。运行时注解,我们可以通过反射获取注解信息,进而进行相应处理。而编译时注解就需要使用注解处理器(Annotation Processor)进行处理。那什么是注解处理器?
注解处理器是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这些生成的Java代码是在新生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。
要实现一个注解处理器需要继承AbstractProcessor,并重写它的4个方法,同时必须要有一个无参的构造方法,以便注解工具能够对它进行初始化。
public class ViewBindProcessor 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();
messager = processingEnv.getMessager();
}
@Override
public Set getSupportedAnnotationTypes() {
//添加需要处理的注解
Set annotataions = new LinkedHashSet();
annotataions.add(MyAnnotation.class.getCanonicalName());
return annotataions;
}
@Override
public SourceVersion getSupportedSourceVersion() {
//指定使用的Java版本
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
//这里实现注解的处理,重点方法
return false;
}
}
- init,会被注解处理工具调用,参数ProcessingEnvironment提供了Elements,Types,Filer,Messager 等。
- getSupportedAnnotationTypes(),指定注解处理器要处理哪些注解,返回一个字符串集合,包含要处理注解的全名。
- getSupportedSourceVersion, 指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
- process,相当于每个处理器的main函数,在这里可以做扫描、评估和处理注解代码的操作,以及生成Java文件。
那么init方法中ProcessingEnvironment提供的Elements,Types,Filer,Messager 等是做什么用的呢?
- Elements:用来处理程序元素的工具类
- Types:用来处理类型数据的工具类
- Filer:用来给注解处理器创建文件
- Messager:用来给注解处理器报告错误,警告,提示等消息。
AutoService注册注解处理器
以前要注册注解处理器是要在module的META-INF目录下新建services目录,并创建一个名为javax.annotation.processing.Processor的文件,然后在文件中写入要注册的注解处理器的全名,例如在javax.annotation.processing.Processor的文件中加上
com.pinery.compile.ViewBindProcessor
来注册ViewBindProcessor注解处理器。
后来Google推出了通过添加AutoService注解库来实现注解处理器的注册,通过在你的注解处理器上加上@AutoService(Processor.class)注解,即可在编译时生成 META-INF/services/javax.annotation.processing.Processor 文件。
配置AutoService需要在工程的build.gradle中添加
JavaPoet生成Java代码
JavaPoet是Square公司出品的生成Java源文件库,正如其名,会写Java代码的诗人,使用它的一系列API就可以很方便的生成java源代码了。
JavaPoet中有几个常用的类:
- MethodSpec,代表一个构造函数或方法声明。
- TypeSpec,代表一个类,接口,或者枚举声明。
- FieldSpec,代表一个成员变量,一个字段声明。
- JavaFile,包含一个顶级类的Java文件。
这是一个计算从1到100相加的方法
public static int caculateNum() {
int result = 0;
for(int i = 1; i < 100; i++) {
result = result + i;
}
return result;
}
我们用MethodSpec实现这个方法声明
MethodSpec caculateMethod = MethodSpec.methodBuilder("caculateNum")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(int.class)
.addStatement("int result = 0")
.beginControlFlow("for(int i = $L; i < $L; i++)", 1, 100)
.addStatement("result = result $L i", "+")
.endControlFlow()
.addStatement("return result")
.build();
可以发现,通过addModifiers添加方法修饰符,returns来定义方法的返回值类型,addStatement来添加方法中的一行语句,它会处理分号和换行,beginControlFlow和endControlFlow构成一个封闭的控制语段,适用于if,for,while等。$L相当于一个占位符,代表的是一个字面量,其他还有:
- $S for Strings,代表一个字符串
- $T for Types,代表一个类型,使用它会自动import导入包
- $N for Names,代表我们自己生成的方法名或者变量名等等
例如
addStatement("$T.out.println($S)", System.class, "Hello World"))
这是定义的一个属性
private final String name = "Pinery";
我们用MethodSpec实现这个方法声明
FieldSpec nameField = FieldSpec.builder(String.class, "name")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.initializer("$S", "Pinery")
.build();
下面是一个类的定义
public class MyClass{
private final String name = "Pinery";
public static int caculateNum() {
int result = 0;
for(int i = 1; i < 100; i++) {
result = result + i;
}
return result;
}
}
我们用TypeSpec实现这个方法声明
TypeSpec helloWorld = TypeSpec.classBuilder("MyClass")
.addModifiers(Modifier.PUBLIC)
.addField(nameField)
.addMethod(caculateMethod)
.build();
通过TypeSpec添加了属性实现和方法实现,其他常用的还有
- addTypeVariable,添加泛型声明
- addSuperinterface,添加接口实现
- addJavadoc,添加注释
- interfaceBuilder,生成一个接口
等,详细使用可以参考JavaPoet官方文档
Element元素相关
注解处理工具扫描java源文件,源代码中的每一部分都是程序中的Element元素,如包,类,方法,字段等。每一个元素代表一个静态的,语言级别的结构。源代码其实是一种结构化的文本(例如json文本就是一种结构化的文本),因此需要对它进行解析,解析的话,解析器会解析某些信息代表某些结构,例如源代码中的类声明信息代表TypeElement类型元素,方法声明信息代表代表ExecutableElement类型元素。有了这些结构,就能完整的表示整个源代码信息了。
Element元素分为以下类型:
- ExecutableElement: 可执行元素,包括类或接口的方法、构造方法或初始化程序
- PackageElement: 包元素,提供对有关包及其成员的信息的访问
- TypeElement: 类或接口元素,提供对有关类型及其成员的信息的访问
- TypeParameterElement: 表示一般类、接口、方法或构造方法元素的形式类型参数,类型参数声明一个 TypeVariable
- VariableElement: 表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数。
Element元素还有个asType()可以获取元素类型TypeMirror,TypeMirror有以下具体类型:
- ArrayType: 表示一个数组类型。多维数组类型被表示为组件类型也是数组类型的数组类型。
- DeclaredType: 表示某一声明类型,是一个类 (class) 类型或接口 (interface) 类型。这包括参数化的类型(比如 java.util.Set
)和原始类型。 - ErrorType: 表示无法正常建模的类或接口类型。这可能是处理错误的结果。大多数对于派生于这种类型(比如其成员或其超类型)的信息的查询通常不会返回有意义的结果。
- ExecutableType: 表示 executable 的类型。executable 是一个方法、构造方法或初始化程序。
- NoType: 在实际类型不适合的地方使用的伪类型。NoType 的种类有:
- VOID:对应于关键字 void。
- PACKAGE:包元素的伪类型。
- NONE:用于实际类型不适合的其他情况中;例如,java.lang.Object 的超类。
- NullType: 表示 null 类型。此类表达式 null 的类型。
- PrimitiveType: 表示一个基本类型。这些类型包括 boolean、byte、short、int、long、char、float 和 double。
- ReferenceType: 表示一个引用类型。这些类型包括类和接口类型、数组类型、类型变量和 null 类型。
- TypeVariable: 表示一个类型变量。类型变量可由某一类型、方法或构造方法的类型参数显式声明。
- WildcardType: 表示通配符类型参数。
编译时注解解析
有了上面知识点的了解之后,下面就可以进行编译时的注解解析了,需要以下几个步骤:
- 定义注解
- 定义一个继承自AbstractProcessor的注解处理器,重写它4个方法中
- 使用AutoService注册自定义的注解处理器
- 实现process方法,在这里处理注解
- 处理所有注解,得到TypeElement和注解信息等信息
- 使用JavaPoet生成新的Java类
在annotations的moudle中定义一个注解
/**
* 作用于View的注解,如@BindView(R.id.text) TextView tvText
*
* @Retention(RetentionPolicy.CLASS) 表示生命周期到类的编译时期
* @Target(ElementType.FIELD) 表示注解作用在字段上
*
* Created by Administrator on 2017/12/31 0031.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
@android.support.annotation.IdRes
int value();
}
在compile的moudle中定义一个ViewBindProcessor注解处理器
/**
* 自定义的AbstractProcessor,用于编译时处理注解
*/
@AutoService(Processor.class) //添加AutoService注解,自动注册ViewBindProcessor注解处理器
public class ViewBindProcessor extends AbstractProcessor{
private Map> bindMap = new HashMap<>();
//用来处理类型数据的工具类
private Types typeUtils;
//用来处理程序元素的工具类
private Elements elementUtils;
//用来给注解处理器创建文件
private Filer filer;
//用来给注解处理器报告错误,警告,提示等消息。
private Messager messager;
/**
* 会被注解处理工具调用,参数ProcessingEnvironment提供了Elements,Types,Filer,Messager 等。
* @param processingEnvironment
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
/**
* 指定注解处理器是注册给那一个注解的,它是一个字符串的集合,意味着可以支持多个类型的注解,并且字符串是合法全名。
* @return
*/
@Override
public Set getSupportedAnnotationTypes() {
Set annotataions = new LinkedHashSet();
//添加自定义的BindView注解
annotataions.add(BindView.class.getCanonicalName());
return annotataions;
}
/**
* 指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 相当于每个处理器的main函数,在这里可以做扫描、评估和处理注解代码的操作,以及生成Java文件。
* @param set
* @param roundEnvironment
* @return
*/
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
collectBindViewAnnotations(roundEnvironment);
generateJavaFilesWithJavaPoet();
return false;
}
/**
* 收集BindView注解
* @param roundEnvironment
* @return
*/
private boolean collectBindViewAnnotations(RoundEnvironment roundEnvironment){
//查找所有添加了注解BindView的元素
Set extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
if(elements == null || elements.isEmpty()){
return false;
}
for(Element element : elements){
//注解BindView必须添加在属性上
if(element.getKind() != ElementKind.FIELD){
error(element, "只有类的属性可以添加@%s注解", BindView.class.getCanonicalName());
return false;
}
//获取注解的值
int viewId = element.getAnnotation(BindView.class).value();
//这个元素是属性类型的元素
VariableElement viewElement = (VariableElement) element;
//获取直接包含属性元素的元素,即类元素
TypeElement typeElement = (TypeElement) viewElement.getEnclosingElement();
//将类型元素作为key,保存到bindMap暂存
List viewBindInfoList = bindMap.get(typeElement);
if(viewBindInfoList == null){
viewBindInfoList = new ArrayList<>();
bindMap.put(typeElement, viewBindInfoList);
}
info("注解信息:viewId=%d, name=%s, type=%s", viewId, viewElement.getSimpleName().toString(), viewElement.asType().toString());
viewBindInfoList.add(new ViewBindInfo(viewId, viewElement.getSimpleName().toString(), viewElement.asType()));
}
return true;
}
/**
* 生成注解处理之后的Java文件
*/
private void generateJavaFilesWithJavaPoet(){
if(bindMap.isEmpty()){
return;
}
//针对每个类型元素,生成一个新文件,例如,针对MainActivity,生成MainActivity_ViewBind文件
for(Map.Entry> entry : bindMap.entrySet()){
TypeElement typeElement = entry.getKey();
List list = entry.getValue();
//获取当前类型元素所在的包名
String pkgName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
//获取类的全名称
ClassName t = ClassName.bestGuess("T");
ClassName viewBinder = ClassName.bestGuess("com.pinery.bind_lib.ViewBinder");
//定义方法结构
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
.addAnnotation(Override.class)//Override注解
.addModifiers(Modifier.PUBLIC)//public修饰符
.returns(void.class)//返回类型void
.addParameter(t, "activity")//参数类型
;
//为方法添加实现
for(ViewBindInfo info : list){
methodSpecBuilder.addStatement("activity.$L = activity.findViewById($L)", info.viewName, info.viewId);
}
//定义类结构
TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + "_ViewBinder")
.addModifiers(Modifier.PUBLIC)//public修饰符
.addTypeVariable(TypeVariableName.get("T", TypeName.get(typeElement.asType())))//泛型声明
.addSuperinterface(ParameterizedTypeName.get(viewBinder, t))
.addMethod(methodSpecBuilder.build())//方法
.build();
//定义一个Java文件结构
JavaFile javaFile = JavaFile.builder(pkgName, typeSpec).build();
try {
//写入到filer中
javaFile.writeTo(filer);
}catch (Exception ex){
ex.printStackTrace();
}
}
}
/**
* 错误提示
* @param element
* @param msg
* @param args
*/
private void error(Element element, String msg, Object... args){
//输出错误提示
messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
}
/**
* 信息提示
* @param msg
* @param args
*/
private void info(String msg, Object... args) {
messager.printMessage(
Diagnostic.Kind.NOTE,
String.format(msg, args));
}
}
这里会使用一个ViewBindInfo用于存储view的id, 名称,和类型的对应关系,如下:
public class ViewBindInfo {
public int viewId;
public String viewName;
public TypeMirror typeMirror;
public ViewBindInfo(int viewId, String viewName, TypeMirror typeMirror){
this.viewId = viewId;
this.viewName = viewName;
this.typeMirror = typeMirror;
}
}
这样就完成了编译时注解的处理。接下来在MainActivity中使用注解
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_text)
TextView tvText;
@BindView(R.id.btn_text)
Button btnText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindHelper.bind(this);
tvText.setText("Hello, BindView");
btnText.setText("Hello, BindView");
}
}
编译后会生成一个MainActivity_ViewBinder.java文件。我们在看看BindHelper的实现
public class BindHelper {
/**
* 绑定方法
* @param activity
*/
public static void bind(Activity activity) {
try {
Class> viewBinderClazz = Class.forName(activity.getClass().getCanonicalName() + "_ViewBinder");
ViewBinder viewBinder = (ViewBinder) viewBinderClazz.newInstance();
viewBinder.bind(activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
这里就会通过反射生成MainActivity_ViewBinder的对象,调用bind方法作view的绑定处理。
例子项目
CompileAnnotation