写在前面
今年大家都在搞组件化,组件化开发不可避免的需要用到路由(Router)来完成组件之间数据的交互,这就促进了各种路由发展如:以及等优秀的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 }}复制代码3. @SuppressWarnings:抑制编译器警告(用的比较少)。Java代码编译时IDE往往会给开发者很多警告信息,例如变量没有使用等,这种警告多了之后很大程度上影响我们debug效率。此注解就是来抑制这些警告。举个栗子:
@SuppressWarning("unused") public void foo() { String s; }复制代码
如果不使用@SuppressWarning来抑制编译器警告,上面的代码会被警告变量s从未使用。出了"unused",该注解支持的抑制类型还有下图的内容(注该图摘自)。
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中使用呢?例如:
然后你发现Crash了。。。这就要引入下一节的内容了,使用apt对被注解的代码进行二次加工。2. 解析Annotation
完成自定义Annotation后,我们还需要知道,针对这些注解,我们要做哪些相关的处理,这就涉及到了Annotation的解析操作。 解析Annotation,通常分为:对运行时Annotation的解析、对编译时Annotation的解析; 解析Annotation,其实就是如何从代码中找到Annotation,通常我们的做法是:
- 用反射的方式获取Annotation,运行时Annotation的解析方式
- 借助apt工具获取Annotation,编译时Annotation的解析方式
- 另外如果我们需要生成额外的代码、文件,则还需要借助
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学会两个库及一个类基本就足够了
- 这个库的主要作用就是帮助我们通过类调用的形式来生成代码,简单理解就是利用这个库可以生成额外的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 set, RoundEnvironment roundEnvironment) { return false; } @Override public SetgetSupportedAnnotationTypes() { 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,具体使用方法可以直接参考
package com.example; // PackageElementpublic 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> cacheElements = null; /** * 缓存所有父Element * key:父Element类名 * value:父Element */ private HashMap cacheAllParentElements = null; @Override public Set getSupportedAnnotationTypes() { // 规定需要处理的注解类型 return Collections.singleton(BindView.class.getCanonicalName()); } @Override public boolean process(Set annotations , RoundEnvironment roundEnv) { //扫描所有注解了BindView的Field,因为我们所有注解BindView的地方都是一个Activity的成员 Set 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 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 childElements = cacheElements.get(parentElementName); childElements.add(childElement); } else { ArrayList 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的问题,请移步
- 待补充ing...
参考文章
About Me
contact way | value |
---|---|
weixinjie1993@gmail.com | |
W2006292 | |
github | https://github.com/weixinjie |
blog | https://juejin.im/user/57673c83207703006bb92bf6 |