Android自定义注解

414 阅读7分钟

按照Tatget来分的话,注解有10种类型。Target是声明该注解使用的地方,比如使用在属性上是field,使用在方法上是method,这里就不一一说明了,感兴趣的同学可以深入研究一下其他几种。按照Retention来分的话,注解有3种类型,分别是SOURCE(源码)、CLASS(编译)、RUNTIME(运行)。本文主要按照Retention,也就是生命周期方向来区别讲解注解。源码注解在编译时使用,比如@overide,生命周期短只在编译时有作用,一般用在IDE代码检测;编译注解缺点是只在编译时有作用,优点是效率比运行时注解高,但是使用较复杂一些;运行时注解优点是在编译时和运行时都能工作,但是运行时通过反射来获取注解的值,效率较低。下面的两个图分别是系统定义的这2个方向类别枚举:




1.源码期注解(RetentionPolicy.SOURCE)

源码注解一般在编译器IDE中使用,比如@overide。源码注解只在编译前起作用,一般用来约束代码规范,在实际的业务开发中并不常见。

2.编译期注解(RetentionPolicy.CLASS)

编译期注解一般结合autoService+javapoet一起使用,用来在编译期生成简单重复的文件,节约手写的时间。下面我们一起来生成一个编译期注解,注意这里一定要新建一个java library来操作,因为核心的注解处理器父类AbstractProcessor类只在java library中有,在Android中是找不到的。假设我们需要生成的效果如下:


第一步 定义注解类

第二步 写好注解处理器

注解处理器通过继承AbstractProcessor的方式实现,在编译的时候系统会寻找到所有AbstractProcessor子类执行其中的process方法,所以我们可以在process中进行生成java文件的操作,生成以后系统会将生成的java文件打包成class。

1.其中需要注意的是,这里需要导入autoservice类库,然后注解处理器类需要通过@AutoService(Processor.class)进行标注。这是固定写法,意思是将该类的路径告诉注解处理器,不然系统不知道处理器路径是没法生成文件的。在编译后,这个路径就会被保存在javalibrary的build/META-INF下面,其中内容就是我们自定义注解处理器的完整路径。当然如果不用@AutoService标注,直接在META-INF目录下新建文件也是可以的,只是更麻烦,使用@AutoService方式进行标注的使用方式如下图:

implementation 'com.google.auto.service:auto-service:1.0-rc2'

2.标注好了一般需要复写其中的4个方法,分别是init、getSupportedAnnotationTypes、getSupportedSourceVersion、process。其中process是最重要的,也是最复杂的。我们先复写前面3个,process我们单独拿出来写。前面3个方法复写如下:

其中init主要是拿到注解处理类。其中最重要的是拿到filer,这个在写文件的时候要用到,所以必须要复写init拿到这个filer。elementUtils主要是用来获取到使用了注解的类基本信息。getSupportedAnnotationTypes是用来告诉系统编译时支持哪些注解,在这里我们把我们自定义的注解丢进去。getSupportedSourceVersion一般就写SourceVersion.latestSupported就好了。再提一句,这个getSupportedAnnotationTypes和getSupportedSourceVersion方法也可以不复写,但是要在MyAbstractor类上用注解的方式标明。

3.接下来我们来写最重要的process方法,这个方法是整个apt最核心的部分。这里可以完全手写但是容易错,速度也慢,所以一般导入javapoet类库,借助javapoet来写。我们process方法实现如下图,在process方法中首先遍历所有注解,找到我们自定义的MyAnotation注解。然后拿到MyAnotation对象,获取到使用处给予的注解的值。接下来通过javapoet中的TypeSpec来拼接出需要生成的java类。最终调用最核心的方法JavaFile.builder(生成的路径,拼接好的格式).build().writeTo(注解处理器的处理对象)。这样就写好了注解处理器的内容,接下来就坐等使用和编译生成就好啦!这里需要注意的是,由于注解处理器是在java library中,所以不能通过Log类来打点,这里可以通过java的输出方式System.out.print()的方式打印出数据来进行调试,也可以通过从init方法中拿到Message对象来打日志,然后在gradle console窗口进行查看日志信息

compile 'com.squareup:javapoet:1.7.0'

提问:核心的process方法中两个参数分别代表什么?

第一个参数是set<TypeElement>,其中TypeElement代表类元素,也就是所有使用了指定注解的所有类,这个指定注解是指上面getSupportedAnotationTypes指定的注解,比如说本例子中在MainActivity中使用了MyAnotation,由于MyAnotation在指定的注解中,所以MainActivity这个类就会出现在该集合中。

第二个参数roundEnviroment是保存了使用了指定注解的所有注解项信息,不管是使用在类上还是属性上还是方法上,都会放到该参数中。在本例子中我们获取到所有使用了MyAnotation注解的注解项,并且取出了注解的值。

第三步 使用注解

由于注解是在javalibrary中定义的,我们首先要将该library导入到app模块中。需要注意的是类库声明和注解声明都需要做,不然是没法跑起来的。然后我们给类中的随意一个属性上加上我们的注解,当然方法和类也是可以的,这里就不一一演示了。注解使用以后我们接下来在onCreate方法里面调用这个通过注解处理器生成的MySimpleAptName类中的test静态方法来测试一下是否可以调用到,需要注意的是我们项目里根本没有去手写这个类,所以直接这样写报错也很正常,一定要rebuild以后等该类生成了那么就不会报错了。

implementation project(':myapt')
annotationProcessor project(':myapt')


第四步 自动生成文件

在运行项目或者rebuild的时候,会在使用时指定好的目录生成class文件,注意这里clean的话是不能生成的。最后生成的效果如下,是不是和我们自己写出来的类很像呢?这个时候回过头去看MainActivity里已经没有报错了,至此编译时注解demo圆满完成。


3.运行期注解(RetentionPolicy.RUNTIME)

运行期注解的生命周期最长,可以存活到程序运行时,也是使用最广泛的注解类型。一般用来代替项目中重复简单的代码,设置一个注解可比写很多无用代码要简洁得多。编译器注解可以通过反射来获取到注解类,拿到我们使用注解时设置的内容。不过由于使用反射,所以对效率会有一些影响,这也是很多开源项目(BufferKnief、Retrofit等)选择编译期注解的原因。不过写起来很简单,只需要三步就OK啦!不像编译期注解要结合其他功能类,接下来我们一起来手写一个运行期注解。

第一步 定义注解类


第二步 使用注解类

第三步 解析注解类

我们需要在运行的时候对注解进行解析。如上图,我们在页面的oncreate方法执行时首先获取到该类所有的注解,获取的值是一个Field[]数组。这里面每一个Field都代表MainActivity类中的一个注解使用项,接下来我们遍历所有的注解项,对自己关心的项进行具体的处理。demo中我们对TextView进行了找控件的操作,这样就统一处理了从而避免了项目中出现很多findViewById的操作,让我们的代码变得更简洁了。

最后:本人小萌新,如果本文有写错的或者描述不全的地方还望各位大佬多多指点,一起探讨,共同进步!