博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android Annotation扫盲笔记
阅读量:6161 次
发布时间:2019-06-21

本文共 14872 字,大约阅读时间需要 49 分钟。

写在前面

今年大家都在搞组件化,组件化开发不可避免的需要用到路由(Router)来完成组件之间数据的交互,这就促进了各种路由发展如:以及等优秀的Router框架。为了方便大家的开发这些Router库以及像ButterKnife这类的库都用到了注解技术。本篇目的是进行一波扫盲。

本文导读

  • Android Annotation基础
  • 解析Annotation
  • 实战-自己做一个ButterKnife

1 Annotatin基础

1.1 基本Annotation

Java提供了三个基本的Annotation注解,使用时需要在Annotation前面增加@符号,并把Annotation当成一个修饰符来使用。注:Java提供的基本Annotation注解都在java.lang包下面

  1. @Override:限定重写父类方法:这个注解主要是命令编译器帮助我们检查代码(避免出现方法名写错这种低级错误),子类使用了Override注解之后,编译的时候IDE会检查父类中是否有这个函数,如果没有这个函数会编译失败。
public class Animal {    public void run() {        //TODO    }}复制代码
public class Monkey extends Animal {    @Override    public void run() {        //使用了OVerride注解之后,必须重写父类方法    }}复制代码
  1. @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了)

  1. @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)

  1. @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;}复制代码
  1. @Documented注解,该注解修饰的自定义注解可以使用javac命令提取成API文档。
  2. @Inherited注解,该注解修饰的自定义具有继承性。举个例子Animal类使用了@Inherited修饰的自定义注解,则子类Monkey也具有该自定义注解描述的特性。

1.3 自定义注解

  1. 定义Annotation,以上面使用的自定义BindView注解为例。可以直接新建Annotation类型的java文件。

  1. 根据自己的需要,使用1.2的只是对自定义的注解进行修饰
/** * Created by will on 2018/2/4. */@Documented//此注解修饰的是属性@Target(ElementType.FIELD)//此注解的作用域是Class,也就是编译时@Retention(value = RetentionPolicy.CLASS)public @interface BindView { }复制代码
  1. 定义成员变量,自定义注解的成员变量以方法的形式来定义。丰富一下上面的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();}复制代码
  1. 使用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)

  1. 新建一个test自定义注解
/** * Created by will on 2018/2/4. */@Documented//此注解修饰的是属性@Target(ElementType.FIELD)@Retention(value = RetentionPolicy.RUNTIME)public @interface test {    int id() default 0;}复制代码
  1. 新建一个java类Animal,并添加test注解
public class Animal {    @BindView(id = 1000)    String a;    @Deprecated    public void run() {        //TODO    }}复制代码
  1. 可以使用反射来获取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介绍

  1. 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 Set
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。
  1. 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

  1. 这个项目用来定义所有自定义的注解,这部分用到了第一节的知识基础。

  1. 在这个项目包里面新建自定义的注解,我们模仿ButterKnife,这里增加一个BindView的注解

@Documented//此注解修饰的是属性@Target(ElementType.FIELD)//此注解的作用域是Class,也就是编译时@Retention(value = RetentionPolicy.CLASS)public @interface BindView {    int id() default 0;}复制代码

2 新建Java项目,名称为annotations_compiler

  1. 这个项目是用来处理自定义注解的,这里姑且叫这个项目为BindView的处理器,这里需要第二节的知识基础
  2. 在build.gradle文件中添加AutoService与JavaPoet的依赖
implementation 'com.google.auto.service:auto-service:1.0-rc2'implementation 'com.squareup:javapoet:1.7.0'复制代码
  1. 新建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项目,使用自定义的注解

  1. 添加对上述两个项目的引用

注意:Android Gradle插件2.2版本发布后,Android 官方提供了annotationProcessor来代替android-apt,annotationProcessor同时支持 javac 和 jack 编译方式,而android-apt只支持 javac 方式。同时android-apt作者宣布不在维护,这里我直接用了annotationProcessor

implementation project(':annotations')annotationProcessor project(':annotations_compiler')复制代码
  1. 在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));    }}复制代码
  1. 此时你的IDE可能会报Bind_MainActivity找不到,没关系,重新Build一下就好了。Build一下后在app/build/generated/source/apt/debug/[你的包名]/annotation/路径下就回生成apt输出的文件了。

其他的问题

  1. 如果你发现build后没有apt文件输出,呵呵,因为你写的processor有Bug~~~。这时候你需要debug你的processor。关于如何debug,请移步
  2. 关于android-apt切换为官方annotationProcessor的问题,请移步
  3. 待补充ing...

参考文章

About Me

contact way value
mail weixinjie1993@gmail.com
wechat W2006292
github https://github.com/weixinjie
blog https://juejin.im/user/57673c83207703006bb92bf6

转载地址:http://veefa.baihongyu.com/

你可能感兴趣的文章
我的友情链接
查看>>
android开源项目框架大全:《IT蓝豹》
查看>>
最小二乘法拟合圆公式推导及vc实现[r]
查看>>
使用Windows API获取和改变当前显示设置
查看>>
(原創) 用OOP实作矩阵相乘 (C/C++)
查看>>
Mozilla FireFox 2.0 简体中文版发布
查看>>
2006年最变态语录
查看>>
WM5/6常见进程列表
查看>>
信息系统开发平台OpenExpressApp - 支持日志功能
查看>>
什么是PIC单片机
查看>>
淘宝Web服务器Tengine正式开源
查看>>
Windows Azure Cloud Service (24) 在模拟器中运行时跳过Windows Azure Startup任务
查看>>
解决asp.net中使用FckEditor上传文件,中文名称乱码的问题
查看>>
POJ 3304 Segments
查看>>
linux shell if 参数
查看>>
Overview of package util.concurrent Release 1.3.4.
查看>>
fastmm使用
查看>>
初步接触XCode和IPhone Simulator
查看>>
委托、Lambda表达式、事件系列06,使用Action实现观察者模式,体验委托和事件的区别...
查看>>
使用HTML5和CSS3碎语
查看>>