写在前面
今年大家都在搞组件化,组件化开发不可避免的需要用到路由(Router)来完成组件之间数据的交互,这就促进了各种路由发展如:阿里的ARouter以及ActivityRouter等优秀的Router框架。为了方便大家的开发这些Router库以及像ButterKnife这类的库都用到了注解技术。本篇目的是进行一波扫盲。
本文导读
- Android Annotation基础
- 解析Annotation
- 实战-自己做一个ButterKnife
1 Annotatin基础
1.1 基本Annotation
Java提供了三个基本的Annotation注解,使用时需要在Annotation前面增加@符号,并把Annotation当成一个修饰符来使用。注:Java提供的基本Annotation注解都在java.lang包下面
- @Override:限定重写父类方法:这个注解主要是命令编译器帮助我们检查代码(避免出现方法名写错这种低级错误),子类使用了Override注解之后,编译的时候IDE会检查父类中是否有这个函数,如果没有这个函数会编译失败。
public class Animal {
public void run() {
//TODO
}
}
复制代码
public class Monkey extends Animal {
@Override
public void run() {
//使用了OVerride注解之后,必须重写父类方法
}
}
复制代码
- @Deprecated:标记该类或者方法已经过时。修改上面的Animal类,使用Deprecated修饰run方法后,子类在使用run方法是IDE会报警告。
public class Animal {
@Deprecated
public void run() {
//TODO
}
}
复制代码
@SuppressWarning("unused")
public void foo() {
String s;
}
复制代码
如果不使用@SuppressWarning来抑制编译器警告,上面的代码会被警告变量s从未使用。出了"unused",该注解支持的抑制类型还有下图的内容(注该图摘自IBM Knowledge Center)。
1.2 JDK元Annotation
JDK出了在java.lang包中提供了1.1介绍的几种基本Annotation外还在java.lang.annotation包下面提供了四个Meta Annotation(元Annotation)。这四种元Annotation都是来修饰自定义注解的。(hold住节奏,看完这个小结咱们就可以自定义Annotation了)
- @Retention注解。该注解只能修饰一个Annotation定义,用于指定所修饰的Annotation可以保留多长"时间"(也可是说是保留的周期)。这里说的“时间”有三种类型
- RetentionPolicy.SOURCE:没啥用,编译器会直接忽略这种策略的注释
- RetentionPolicy.CLASS:自定义注解的默认值,编译器会把这种策略的注释保存在class文件中。像ButterKnife中的BindView注解就是用的这种方式。
- RetentionPolicy.RUNTIME:编译器会把该策略的注释保存到class文件中,程序可以通过反射等方式来获取。
举个例子,自定义一个BindView注解(看不懂没关系,现有一个感性的认识,下一节开始做自定义Annotation讲解)。
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
int id() default 0;
}
复制代码
当成员变量为value时,可以省略。也就是说上述代码可以换成 @Retention(RetentionPolicy.CLASS)
- @Target注解:这货也是用于修饰一个自定义的Annotation注解,用于指定自定义注解可以修饰哪些程序元素。该注解的成员变量有
- ElementType.PACKAGE 注解作用于包
- ElementType.TYPE 注解作用于类型(类,接口,注解,枚举)
- ElementType.ANNOTATION_TYPE 注解作用于注解
- ElementType.CONSTRUCTOR 注解作用于构造方法
- ElementType.METHOD 注解作用于方法
- ElementType.PARAMETER 注解作用于方法参数
- ElementType.FIELD 注解作用于属性
- ElementType.LOCAL_VARIABLE 注解作用于局部变量 同样的,成员变量名为value时可以省略。我们丰富一下上面用到的自定义的BindView注解:
//此注解修饰的是属性
@Target(ElementType.FIELD)
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
int id() default 0;
}
复制代码
- @Documented注解,该注解修饰的自定义注解可以使用javac命令提取成API文档。
- @Inherited注解,该注解修饰的自定义具有继承性。举个例子Animal类使用了@Inherited修饰的自定义注解,则子类Monkey也具有该自定义注解描述的特性。
1.3 自定义注解
- 定义Annotation,以上面使用的自定义BindView注解为例。可以直接新建Annotation类型的java文件。
- 根据自己的需要,使用1.2的只是对自定义的注解进行修饰
/**
* Created by will on 2018/2/4.
*/
@Documented
//此注解修饰的是属性
@Target(ElementType.FIELD)
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
}
复制代码
- 定义成员变量,自定义注解的成员变量以方法的形式来定义。丰富一下上面的BindView,由于这个自定义注解的功能是对Activity中的View进行绑定。所以我们定义一个id成员变量。
/**
* Created by will on 2018/2/4.
*/
@Documented
//此注解修饰的是属性
@Target(ElementType.FIELD)
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
int id();
}
复制代码
- 使用default关键字为成员变量指定默认值。继续丰富BindView的代码。注default关键字放到int id() 后面。
@Documented
//此注解修饰的是属性
@Target(ElementType.FIELD)
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
int id() default 0;
}
复制代码
根据有没有成员变量,我们可以将Annotation划分成两种:
- 没有成员变量的注解称为"标记Annotation",这种注解使用自身是否存在为我们提供信息,例如Override等注解
- 有成员变量的称谓"元数据Annotation"。我们可以使用apt等工具对这种Annotation的成员进行二次加工。
注意:只定义了自定义注解没有任何效果,还需要对Annotation的信息进行提取与加工!!!
上面我们自定义了BindView注解,你是不是想直接拿到Activity中使用呢?例如:
2. 解析Annotation
完成自定义Annotation后,我们还需要知道,针对这些注解,我们要做哪些相关的处理,这就涉及到了Annotation的解析操作。 解析Annotation,通常分为:对运行时Annotation的解析、对编译时Annotation的解析; 解析Annotation,其实就是如何从代码中找到Annotation,通常我们的做法是:
- 用反射的方式获取Annotation,运行时Annotation的解析方式
- 借助apt工具获取Annotation,编译时Annotation的解析方式
- 另外如果我们需要生成额外的代码、文件,则还需要借助JavaPoet API
2.1 利用反射解析Annotation
反射的解析方式,通常运用在运行时Annotation的解析。 反射是指:利用Class、Field、Method、Construct等reflect对象,获取Annotation
- field.getAnnotation(Annotation.class):获取某个Annotation
- field.getAnnotations():获取所有的Annotation
- field.isAnnotationPresent(Annotation.class):是否存在该Annotation
通常使用Runtime修饰的注解需要使用反射来配合解析
@Retention(value = RetentionPolicy.RUNTIME)
- 新建一个test自定义注解
/**
* Created by will on 2018/2/4.
*/
@Documented
//此注解修饰的是属性
@Target(ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface test {
int id() default 0;
}
复制代码
- 新建一个java类Animal,并添加test注解
public class Animal {
@BindView(id = 1000)
String a;
@Deprecated
public void run() {
//TODO
}
}
复制代码
- 可以使用反射来获取a的注解成员属性值
private void testMethod() {
Class clazz = Animal.class;
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
BindView bindView = field.getAnnotation(BindView.class);
if (bindView != null) {
int id = bindView.id();
Log.e("------", String.valueOf(id));
}
}
}
复制代码
2.2 使用apt工具来解析Annotation
APT:是一个注解处理工具 Annotation Processing Tool 作用:利用apt,我们可以找到源代中的注解,并根据注解做相应的处理
- 根据注解,生成额外的源文件或其他文件
- 编译生成的源文件和原来的源文件,一起生成class文件
利用APT,在编译时生成额外的代码,不会影响性能,只是影响项目构建的速度
这里我们说一下Android中使用apt的步骤 Android中开发自定义的apt学会两个库及一个类基本就足够了
- JavaPoet API 这个库的主要作用就是帮助我们通过类调用的形式来生成代码,简单理解就是利用这个库可以生成额外的Java代码。具体的API可以去github上看下,写的很详细。这里不贴代码了。
- AutoService 这个库是Google开发的,主要的作用是注解 processor 类,并对其生成 META-INF 的配置信息。可以理解使用这个库之后编译的时候IDE会编译我们的Annotation处理器,只需要在自定义的Processor类上添加注释
@AutoService(Processor.class)
下面会用到。 - Processor类,我们自定义的Annotation处理器都需要实现该接口,Java为我们提供了一个抽象类实现了该接口的部分功能,我们自定义Annotation处理器的时候大部分只需要继承AbstractProcessor这个抽象类就行了。
JavaPoet的学习可以直接借鉴官方api,AutoService学习成本较低(只需要用里面一句代码而已,学习成本可以忽略),下面我们重点学习一下AbstractProcessor的使用。
AbstractProcessor介绍
- AbstractProcessor方法介绍:下面新建一个AbstractProcessor来看下这货的方法
/**
* Created by will on 2018/2/5.
*/
public class CustomProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}
复制代码
- init(ProcessingEnvironment processingEnvironment): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。
- process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。以前面提到的自定义注解BindView为例,这里可以查到所有注解了BindView的Activity。
- getSupportedAnnotationTypes(): 这里必须由开发者指定,该方法返回一个Set,作用是这个注解的处理器支持处理哪些注解。
- getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 7的话,你也可以返回SourceVersion.RELEASE_7。
- AbstractProcessor基础工具解析:从AbstractProcessor的init方法中可以获取一系列的工具来辅助我们解析源码
- Elements工具类 在AbstractProcessor的init方法中可以获取到一个Elements工具类,具体代码为
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
Elements elementUtils = processingEnv.getElementUtils();
}
复制代码
这个工具类是用来处理源代码的,在自定义注解处理器的领域里面,Java源代码每一个类型都属于一个Element,具体使用方法可以直接参考Java官方文档
package com.example; // PackageElement
public class Test { // TypeElement
private int a; // VariableElement
private Test other; // VariableElement
public Test () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}
复制代码
例如,我有一个TypeElement,希望拿到这个class所在的包名就可以使用Elemnts这个工具
private String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
复制代码
再来一个栗子,有一个代表Test的TypeElement,希望获取所有的子元素可以这么写(注意,这个很有用)
TypeElement testClass = ... ;
for (Element e : testClass.getEnclosedElements()){ // iterate over children
Element parent = e.getEnclosingElement(); // parent == testClass
}
复制代码
- Types:一个用来处理TypeMirror的工具类; TypeElement并不包含类本身的信息。你可以从TypeElement中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过TypeMirror获取。你可以通过调用elements.asType()获取元素的TypeMirror。
- Filer:正如这个名字所示,使用Filer你可以创建文件。
好了枯燥的基础知识看完了之后我们一起写一个简单的ButterKnife
3. 自己写一个轻量级的ButterKnife
1. 新建一个Java项目,名字为annotations
- 这个项目用来定义所有自定义的注解,这部分用到了第一节的知识基础。
- 在这个项目包里面新建自定义的注解,我们模仿ButterKnife,这里增加一个BindView的注解
@Documented
//此注解修饰的是属性
@Target(ElementType.FIELD)
//此注解的作用域是Class,也就是编译时
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
int id() default 0;
}
复制代码
2 新建Java项目,名称为annotations_compiler
- 这个项目是用来处理自定义注解的,这里姑且叫这个项目为BindView的处理器,这里需要第二节的知识基础
- 在build.gradle文件中添加AutoService与JavaPoet的依赖
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.7.0'
复制代码
- 新建BindViewProcessor处理器类继承自AbstractProcessor,对源代码的注解进行处理(我尽可能的理解有歧义的地方都添加了注释)
/**
* Created by will on 2018/2/4.
*/
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
/**
* 工具类,可以从init方法的ProcessingEnvironment中获取
*/
private Elements elementUtils;
/**
* 缓存所有子Element
* key:父Element类名
* value:子Element
*/
private HashMap<String, List<Element>> cacheElements = null;
/**
* 缓存所有父Element
* key:父Element类名
* value:父Element
*/
private HashMap<String, Element> cacheAllParentElements = null;
@Override
public Set<String> getSupportedAnnotationTypes() {
// 规定需要处理的注解类型
return Collections.singleton(BindView.class.getCanonicalName());
}
@Override
public boolean process(Set<? extends TypeElement> annotations
, RoundEnvironment roundEnv) {
//扫描所有注解了BindView的Field,因为我们所有注解BindView的地方都是一个Activity的成员
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
for (Element element : elements) {
//将所有子elements进行过滤
addElementToCache(element);
}
if (cacheElements == null || cacheElements.size() == 0) {
return true;
}
for (String parentElementName : cacheElements.keySet()) {
//判断一下获取到的parent element是否是类
try {
//使用JavaPoet构造一个方法
MethodSpec.Builder bindViewMethodSpec = MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(ClassName.get(cacheAllParentElements.get(parentElementName).asType())
, "targetActivity");
List<Element> childElements = cacheElements.get(parentElementName);
if (childElements != null && childElements.size() != 0) {
for (Element childElement : childElements) {
BindView bindView = childElement.getAnnotation(BindView.class);
//使用JavaPoet对方法内容进行添加
bindViewMethodSpec.addStatement(
String.format("targetActivity.%s = (%s) targetActivity.findViewById(%s)"
, childElement.getSimpleName()
, ClassName.get(childElement.asType()).toString()
, bindView.id()));
}
}
//构造一个类,以Bind_开头
TypeSpec typeElement = TypeSpec.classBuilder("Bind_"
+ cacheAllParentElements.get(parentElementName).getSimpleName())
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(bindViewMethodSpec.build())
.build();
//进行文件写入
JavaFile javaFile = JavaFile.builder(
getPackageName((TypeElement) cacheAllParentElements.get(parentElementName))
, typeElement).build();
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
return true;
}
}
return true;
}
private String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 缓存父Element对应的所有子Element
* 缓存父Element
*
* @param childElement
*/
private void addElementToCache(Element childElement) {
if (cacheElements == null) {
cacheElements = new HashMap<>();
}
if (cacheAllParentElements == null) {
cacheAllParentElements = new HashMap<>();
}
//父Element类名
String parentElementName = null;
parentElementName = ClassName.get(childElement.getEnclosingElement().asType()).toString();
if (cacheElements.containsKey(parentElementName)) {
List<Element> childElements = cacheElements.get(parentElementName);
childElements.add(childElement);
} else {
ArrayList<Element> childElements = new ArrayList<>();
childElements.add(childElement);
cacheElements.put(parentElementName, childElements);
cacheAllParentElements.put(parentElementName, childElement.getEnclosingElement());
}
}
}
复制代码
3.新建Android项目,使用自定义的注解
- 添加对上述两个项目的引用
注意:Android Gradle插件2.2版本发布后,Android 官方提供了annotationProcessor来代替android-apt,annotationProcessor同时支持 javac 和 jack 编译方式,而android-apt只支持 javac 方式。同时android-apt作者宣布不在维护,这里我直接用了annotationProcessor
implementation project(':annotations')
annotationProcessor project(':annotations_compiler')
复制代码
- 在Activity的View中添加@BindView注解,并设置id
public class MainActivity extends AppCompatActivity {
@BindView(id = R.id.tv_test)
TextView tv_test;
@BindView(id = R.id.tv_test1)
TextView tv_test1;
@BindView(id = R.id.iv_image)
ImageView iv_image;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Bind_MainActivity.bindView(this);
tv_test.setText("test_1");
tv_test1.setText("test_2");
iv_image.setImageDrawable(getDrawable(R.mipmap.ic_launcher));
}
}
复制代码
- 此时你的IDE可能会报Bind_MainActivity找不到,没关系,重新Build一下就好了。Build一下后在app/build/generated/source/apt/debug/[你的包名]/annotation/路径下就回生成apt输出的文件了。
其他的问题
- 如果你发现build后没有apt文件输出,呵呵,因为你写的processor有Bug~~~。这时候你需要debug你的processor。关于如何debug,请移步这篇博客
- 关于android-apt切换为官方annotationProcessor的问题,请移步android-apt切换为官方annotationProcessor
- 待补充ing...
参考文章
About Me
contact way | value |
---|---|
weixinjie1993@gmail.com | |
W2006292 | |
github | https://github.com/weixinjie |
blog | https://juejin.im/user/57673c83207703006bb92bf6 |