PARAMETER, //方法参数或者构造方法参数
CONSTRUCTOR, //构造方法
LOCAL_VARIABLE, //局部变量
ANNOTATION_TYPE, //注解类型
PACKAGE, //包
TYPE_PARAMETER, //类型参数,1.8加入
TYPE_USE //类型使用 1.8加入
}
将InjectString和InjectInt的@Target设置为ElementType.FIELD,所以他们只能注解成员变量。
(2)使用注解
在app模块的build.gradle中依赖annotions模块:
dependencies {
compile project(‘:annotations’)
}
然后在MainActivity中使用两个注解InjectString和InjectInt分别来注解两个成员变量:
@InjectString
public String hello;
@InjectString
public String world;
@InjectInt
public int one;
@InjectInt
public int two;
在资源文件中创建成员变量同名的字符串和整型资源:
//res/values/ints
1
2
//res/values/strings
Hello
World
我们期望通过注解生成一个对应的Java类来处理资源和成员变量的绑定,假设期望生成的代码如下:
public class MainAcivity_Inject{
public MainActivity_Inject(Context context){
((MainActivity)context).one=context.getResources().getInteger(R.integer.one);
…
}
}
这样就只需要在MainActivity的onCreate方法中调用MainActivity_Inject的构造方法就能完成成员变量的初始化了。
所以接下来的任务就是解析在代码中使用的注解,根据注解生成MainActivity_Inject.java这个文件
(3)解析注解
新建一个Java Library模块命名为compiler,在其中创建一个类InjectProcessor,继承自AbstractProcessor
,AbstractProcessor类为注解处理器的抽象类。在编译阶段,会查找指定的注解处理器实现类,并调用其process方法,完成注解的解析。
//配置注解处理器支持处理的注解类型为InjectString和InjectInt
@SupportedAnnotationTypes({“com.example.annotations.InjectString”,
“com.example.annotations.InjectInt”})
//配置注解处理器支持的Java版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@AutoService(Processor.class)
//定义注解处理器继承自AbstractProcessor
public class InjectProcessor extends AbstractProcessor{
private static final ClassName CONTEXT = ClassName.get(“android.content”, “Context”);
//待生成java文件的的集合,key为被注解的类的类名,value为GenerateJavaFile对象
private HashMap<String,GenerateJavaFile> mGenerateJavaFiles = new HashMap<String, GenerateJavaFile>();
/**
-
实现process方法,完成注解的解析和处理,通常生成文件或者校验处理
-
@param set 定义的注解类型的集合
-
@param roundEnvironment 回合环境,注解的处理可能要经过几个回合的处理,每个回合处理一批注解
-
@return 返回true表示注解被当前注解处理器处理,就不会再交给其他注解处理器
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//遍历所有的TypeElement的,一个注解类型对应一个TypeElement
for (TypeElement typeElement : set) {
//遍历在代码中使用typeElement对应注解类型来注解的元素
//例如:如果typeElement对应的是InjectString注解类型,那么Element对应为使用@InjectString注解的成员变量
for (Element element : roundEnvironment.getElementsAnnotatedWith(typeElement)) {
//添加注解元素到将要生成的java文件对应的GenerateJavaFile的对象中
addElementToGenerateJavaFile(element);
}
}
//生成java文件
createJavaFile();
return true;
}
}
因为之前定义的InjectString和InjectInt是类,所以他们的Element类型是TypeElement,一个TypeElement对应一个注解类型,所以process传入的set集合中有两个TypeElement对象,分别对应InjectString和InjectInt,我们遍历set集合,处理每个注解类型。通过调用roundEnvironment的getElementsAnnotatedWith(typeElement)来获取一个注解类型注解了哪些元素。
比如我们使用了InjectStirng注解了两个字符串成员变量:
@InjectString
public String hello;
@InjectString
public String world;
在InjectStirng的TypeElement会遍历hello和world这两个Element 。因为我们要将注解的元素转化成Java代码,所以需要处理每个Element对象,为生成Java文件做准备。我们定义类GenerateJavaFile来描述一个待生成的Java文件在InjectProcessor中:
/**
- 描述一个待生成的Java文件
*/
private static class GenerateJavaFile {
String packageName;//包名
String className;//类名
List elements;//注解元素集合
}
通过调用addElementToGenerateJavaFile方法创建GenerateJavaFile对象,并将Element对象加入到GenerateJavaFile对象内部的elements集合中。addElementToGenerateJavaFile实现如下:
/**
-
添加一个注解元素到对应的GenerateJavaFile对象中
-
@param element 注解元素
*/
private void addElementToGenerateJavaFile(Element element) {
//获取element对应成员变量所在的类,即被注解的类
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
String[] split = typeElement.getQualifiedName().toString().split(“\.”);
String className = split[split.length - 1];
//通过父类的processingEnv获取报信者,用于在编译过程中打印log
Messager messager = processingEnv.getMessager();
messager.printMessage(Diagnostic.Kind.NOTE, "add element to generate file " + className);
//获取被注解类对应的GenerateJavaFile对象,如果没有,则创建
GenerateJavaFile generateJavaFile = mGenerateJavaFiles.get(className);
if (generateJavaFile == null) {
GenerateJavaFile file = new GenerateJavaFile();
//设置待生成java文件的包名
file.packageName = processingEnv.getElementUtils().getPackageOf(element).toString();
//设置待生成java文件的类名
file.className = className + “_Inject”;
//初始化元素集合
file.elements = new ArrayList();
file.elements.add(element);
//保存被注解类所对应要生成java类的GenerateJavaFile对象
mGenerateJavaFiles.put(className, file);
} else {
//将注解元素添加到有的generateJavaFile对象中
generateJavaFile.elements.add(element);
}
}
(4)生成Java源文件
解析完所有的注解,创建GenerateJavaFile对象后,就可以根据GenerateJavaFile对象来生成对应的Java文件,这里可以使用第三方库javapoet来完成Java文件的生成,它的使用方式可以参考blog,这里不再赘述。
在compiler模块的build.gradle下添加javapoet的依赖同步:
compile ‘com.squareup:javapoet:1.9.0’
接着实现createJavaFile文件方法,来完成Java文件的生成,遍历GenerateJavaFile集合,一个GenerateJavaFile对象对应一个要生成的Java文件,首先创建这个Java类的构造方法:
//createJavaFile()
//构建一个构造方法,该构造方法带有一个Context类型的参数
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(CONTEXT, “context”);
参数类型CONTEXT定义为:
private static final ClassName CONTEXT = ClassName.get(“android.content”, “Context”);
然后遍历注解元素,一个Element对象对应一条赋值代码:
//遍历该类中需要处理的注解元素
for (Element element: file.elements) {
//如果注解的成员变量是一个int类型
if (element.asType().toString().equals(“int”)) {
//在构造方法中添加一条语句
//例如:((MainActivity)context).one = context.getResources().getInteger(R.integer.one);
builder.addStatement(“(( N ) c o n t e x t ) . N)context). N)context).N = context.getResources().getInteger(R.integer.$N)”,
file.className.split(“_”)[0], element.getSimpleName(), element.getSimpleName());
//如果注解的是一个String类型
} else if (element.asType().toString().equals(“java.lang.String”)) {
//在构造方法中添加一条语句
//例如:((MainActivity)context).hello = context.getResources().getString(R.string.hello);
builder.addStatement(“(( N ) c o n t e x t ) . N)context). N)context).N = context.getResources().getString(R.string.$N)”,
file.className.split(“_”)[0], element.getSimpleName(), element.getSimpleName());
}
}
最后构建一个类,输出Java文件:
//构建一个类,添加一个上述的构造方法
TypeSpec typeSpec = TypeSpec.classBuilder(file.className)
.addModifiers(Modifier.PUBLIC)
.addMethod(builder.build())
.build();
try {
//输出java文件
JavaFile javaFile = JavaFile.builder(file.packageName, typeSpec).build();
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
(5)注解处理器,开始构建
注解处理器创建完后,还需要注册,这里最简单的方法是使用AutoService工具注册,在compiler的build.gradle添加依赖:
compile ‘com.google.auto.service:auto-service:1.0-rc3’
然后给InjectProcess添加AutoService注解:
@AutoService(Processor.class)
//定义注解处理器继承自AbstractProcessor
public class InjectProcessor extends AbstractProcessor{
。。。。
}
AutoService的作用是生成一个配置文件来注册注解处理器。该配置文件的内容为com.example.InjectProcessor。
一切准备就绪,我们在app模块下添加依赖:
dependencies{
annotationProcessor project(‘:compiler’)
}
在项目目录terminal下输入:gradle clean和gradle build,来构建一遍项目,构建成功后会在app模块下的路径:build/generated/source/apt/debug/包名 下生成一个MainActivity_Inject.java文件。结果与期望的代码一致:
public class MainAcivity_Inject{
public MainActivity_Inject(Context context){
((MainActivity)context).one=context.getResources().getInteger(R.integer.one);
((MainActivity)context).two=context.getResources().getInteger(R.integer.two);
((MainActivity)context).hello=context.getResources().getInteger(R.integer.hello);
((MainActivity)context).world=context.getResources().getInteger(R.integer.world);
}
}
到这里就完成一个自定义的注解器啦,最后我们将one、two输出在屏幕上得出的就是1、2。
从结果来看注解器帮我们做的事情就是 另one=1、tow=2、hello=‘hello’,world=‘world’这样子。
我们再来对自定义注解器的过程做一个小结:
- 定义注解
创建一个Java Library,在这里通过@Interface来定义注解类,并声明它要修饰的变量类型(类、成员变量等等)。
- 在Activity去使用注解
在build.gradle中添加依赖的模块,并在Activity中使用注解绑定一个控件或者变量。
- 解析注解(最关键的一步)
创建一个Java Library作为注解器,表明支持的注解类型,并且注解类要继承AbstractProcessor, 实现其process方法,在process方法中会传入一个set和roundEnviroment回合环境,set里面是注解类型的集合,通过遍历set,我们所有被注解的对象加入到自己准备的文件中。这个文件存储所有被注解的Element。
- 生成Java源文件
在创建完文件后,我们需要在程序编译时让这个文件生成一个正式的文件类,就是类似于MainActivity_ViewBinding那样的文件类,我们通过第三方库javapoet来完成Java文件的生成。通过MethodSpec.Build为这个类设置构造方法,并格式化代码(如为每个Element使用FindViewById)。最后输出这个Java文件。
- 注册注解处理器
通过第三方库AutoService来为当前项目注册这个注解处理器。注册完后就可以使用编译时注解了。
下面来分析ButterKnife的源码,ButterKnife的Java Library为butter-annotation模块,下面有其所有注解,包括常用的bindView和onClick。
1、注解定义:
@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
结尾
好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。
这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
n模块,下面有其所有注解,包括常用的bindView和onClick。
1、注解定义:
@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
结尾
好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。
这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~
[外链图片转存中…(img-57fibCHv-1714797528782)]
[外链图片转存中…(img-LWBC7Rwx-1714797528784)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!