Android自定义注解处理器详解

Demo GitHub地址:https://github.com/lg2179/AnnotationDemo

案例简述

在我们Android项目中很多第三方库都用到了注解,像我们项目中最常用的butterKnife以及eventBusy以及Retrofit都是以注解为基础进行使用的,通过使用注解能很大程度上节省代码,但同样注解也很容易让初学者产生困惑,特别是在使用这类项目的时候,有时候出现错误会难以调试,主要原因还是很多人并不了解这类框架其内部的原理,所以遇到问题时会消耗大量的时间去排查。其次我们如果有好的想法,发现某些代码需要重复创建,我们也可以自己来写个框架方便自己日常的编码,提升编码效率;最后也算是自身技术的提升,本文我们介绍一下注解的原理以及使用过程,以及自己编写一个View注入的框架来减少我们编写页面时重复使用的findViewById()方法;

概念介绍

1.注解定义

   注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

初学者可以这样理解注解:想像代码具有生命,注解就是对于代码中某些鲜活个体的贴上去的一张标签。简化来讲,注解如同一张标签。

2.注解作用

        1、生成文档。这是最常见的,也是java 最早提供的注解。常用的有@param @return 等
        2、跟踪代码依赖性,实现替代配置文件功能。比如Dagger 2依赖注入,未来java开发,将大量注解配置,具有很大用处;
        3、在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。

3.元注解

      Java.lang.annotation提供了四种元注解,专门注解其他的注解(在自定义注解的时候需要使用到元注解):

         (1)@Documented  注解是否将包含在JavaDoc中

       (2)@Retention     什么时候使用该注解,

RetentionPolicy.SOURCE:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,所以他们不会写入字节码,像@overrid这类注解。

RetentionPolicy.CLASS:在类加载的时候被丢弃,在字节码文件的处理中有用,注解默认使用这种方式,

RetentionPolicy.RUNTIME:始终不会丢弃,运行期也保留该注              解,因此可以使用反射机制读取该注解的信息。运行时期使用反射可能会影响性能。

       (3)@Target        表示该注解用于什么地方,默认值为任何元素,表示该注解用于任何地方,可用的ElementType参数包括:

                ElementType.CONSTRUCTOR:用于描述构造器

                ElementType.FIELD:成员变量,对象,属性(包括enum实例)     

                ElementType.LOCAL_VARIBALE:用于描述局部变量

                ElementType.METHOD:用于描述方法

                ElementType.PACKAGE:用于描述包

                ElementType.PARAMTER:用于描述参数

                ElementType.TYPE:用于描述类,接口(包括注解类型)活enumeration声明

       (4)@Inherited      定义该注解和子类的关系,如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类  

4.自定义注解

     自定义注解类编写需要注意的一些规则:   

        1.Annotation型定义为@interface,所有的Annotation会自动继承java.lang.Annotation这个接口,并且不能再去集成别的类或是接口。  

        2.参数成员只能用public或default这两个访问权限修饰符。

        3.参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和String,Enum, Calss, annotation等数据类型以及一些类型的数组。

        4.要获取类方法和字段的注解信息,必须通过Java的反射技术来获取Annotation对象,因为除此之外没有其他的获取注解对象的方法。

        5.自定义注解需要使用到元注解。   

案例分析      

编写前的准备

在编写注解处理器的时候一般是需要建立多个module来作为功能区分,我们编写的这个View注入的例子是这样来划分功能的。

                                                        

                                                                                       图1.模块划分

apt_annotation   用于存放注解

apt_compiler   用于存放编写的注解处理器

apt_api        注解需要作为api提供给外部使用

  

既然有module那么使用就需要有依赖,因为编写注解处理器需要使用到我们定义的注解,所以apt_compiler需要依赖apt_annotation,我们app模块需要使用到该注解处理器提供出来给外部使用的api,所以app需要依赖apt_api,apt_api同样使用到注解所以需要依赖apt_annotation;

 

注解类编写

注解模块主要用于存放一些注解类,也就是我们自己编写的注解类BindView。本文编写的View注入所以定义一个注解类即可。

                                                    

                                                                                       图2.注解类

    我们编写的是View注入,所以只需要传入View的id即可,用于成员变量View,所以@Target此处为ElementType.FIELD,并且是保留到编译时期,所以使用RetentionPolicy.CLASS,因为id是int型,并且我们只需要获取到id的值即可,所以使用int value()来进行设置。

方法详解

编写完注解类之后就需要编写注解处理器来处理该注解,这里是比较复杂的,讲解之前需要先导入一个auto-service的第三方库,如果不使用这个库也可以,但是需要手动去META-INF中编写文件对注解进行设置。

                                               

                                                                                    图3.导入依赖

如果我们不使用auto-service第三方库就需要手动编写下图这个文件。

                                                                        

                                                                                     图4.META-INF文件

    我们编写的是,不会对性能有任何影响的:编译时注解。也有人叫它代码生成,其实他们还是有些区别的,在编译时对注解做处理,通过注解,获取必要信息,在项目中生成代码,运行时调用,和直接运行手写代码没有任何区别。而更准确的叫法:APT - Annotation Processing Tool。

    首先我们了解一下注解处理器的基本使用方法:

自定义注解处理器必须继承AbstractProcessor抽象类,这个类有一个重要的方法process();除此之外还有三个方法需要我们关注包括init(),getSupportedSpurceVersion()以及getSupportedAnnotationTypes();

我们先看process()方法所有的注解处理都是从process()方法开始,你可以理解为,当APT找到所有需要处理的注解后,会回调这个方法,通过这个方法的参数,能够拿到所需要的信息。这个方法有两个参数我们先看一下。

                                                 

                                                                                       图5.process()方法参数

参数Set<? Extents TypeElement> annotations,这个参数代表返回所有的由该Processor处理,并且待处理的注解Annotations。

我们先来介绍一下注解处理器处理过程,注解处理过程是一个有序的循环过程,每次循环中,一个处理器要求去处理那些在上一次循环中产生的源文件和类文件中的注解,第一次循环的输入是运行此方法的初始化输入,是默认产生的,这些初始输入,可以看成是虚拟的第0次循环的输出,也就是说我们事先的process方法有可能会被调用多次,因为我们生产的文件也有可能会包含我们process中设置接收的注解,例如我们项目中MainActivity第一次循环之后会调用process方法处理我们指定的注解并且产生一个MainActivity_BindView(该注解处理器自定义生成的文件在app->build->generated->source->apt文件夹下面),这个时候就会再一次循环输入MainActivity_BindView,如果MainActivity_BindView里面也包含有注解(比如我们在生成MainActivity_BindView时在里面手动加上Retrofit的注解,但是这个注解必须是getSupportAnnotationTypes中支持的)那么会再次调用process方法来处理这个注解(处理这个注解的前提是需要getSupportedAnnotationTypes()方法指定了该注解),这次输入的输出没有产生新文件,第三次输入为空,输出为空。

介绍了过程那么这两个参数就很好理解了,第一个就是我们指定返回的注解元素,第二个就是上一次循环的信息和环境。返回值表示这些注解是否由此Processor声明,如果返回true,则这些注解已声明并且不再要求后续Processor处理他们,如果返回false,则这些注解未声明并且可能要求后续Processor处理他们。

代码编写过程

初始化设置

注解处理器一般是继承与AbstractProcessor,一般这里有四个方法是固定的格式,我们看一下代码:

                                  

                                                                                           图6.Processor代码

在实现AbstractProcessor后,process()方法时必须实现的,也是我们编写代码的最重要部分,我们先看一般来说比较格式化书写的三个方法。我们首先会复写init()方法,这个方法传入了一个processingEnv参数,这个参数可以帮助我们初始化一些重要的变量,mFiler是后面用来创建生成的java文件的辅助类,mMessager用来打印日志,mElementUtls是跟元素相关的辅助类,通过这个类我们可以获取到元素相关的信息,比如可以通过代表成员变量的元素来获取到代表包的元素。

Process实现

Process的实现,会复杂很多,一般为了便于记忆我们将他们分为两个部分

  1. 收集信息
  2. 生成代理文件(编译时生成的文件)

收集信息其实就是根据你的注解声明,拿到对应的元素Element获取到我们所需要的信息,这个信息肯定是为了后面生成Java文件所准备的。在我们编写的代码中我们会对每一个使用到注解的Activity生成一个代理类,例如MainActivity我们会生成一个MainActivity_ViewBinding代理类,SeconActivity我们会生成一个SecondActivity_ViewBinding代理类,那么如果多个类中声明了注解,就对应多个代理类,那么就需要:

  1. 一个类对象,代表具体某个类的代理类生成的全部信息,本例中是ClassCreatorProxy.
  2. 一个集合,存放上述类对象(到时候遍历生成代理类),本例中为Map<String,ClassCreatorProxy>

我们先来看一下收集信息的的过程:

收集信息:

                               

                                                                                                图7.收集信息

文中对每一行代码的注释已经解释的很清楚了,我们简单介绍一下这个过程,首先我们会调用一下mProxyMap.clear();因为process可能会多次调用所以为了避免生成重复的代理类,我们会先清空一下这个存放代理类的集合,然后通过roundEnv.getElementAnnotatedWith()拿到我们通过@BindView注解的元素,这里返回值因为我们提前知道是用于变量上的所以就是VariableElement集合。

接下来for循环我们的元素,然后拿到对应的类信息TypeElement,继而生成ClassCreatorProxy对象,这里通过一个mProxyMap进行检查,key为fullClassName即类的全路径,如果没有生成才会去生成一个新的,ClassCreatorProxy与类是一一对应。

接下来,会将与该类对应的且被@BindView声明的VariableElement加入到ClassCreatorProxy中去,key为我们声明时填写的Id,即View的id。这样就完成了信息的收集,收集完成信息后,应该就可以去生成代理类了。

 

生成代理类

                              

                                                                                           图8.生成代理类

可以看到生成代理类的代码非常的简短,主要就是遍历我们的mProxyMap,然后取得每一个ClassCreatorProxy,最后通过mFiler来创建文件对象,类名为creatorProxy.getProxyClassFullName(),写入的内容为createProxy.generateJavaCode()。

生成Java代码

我们看一下Java代码生成的方式。

                                       

                                                                                      图9.Java代码生成

生成代码这里比较简单,我们可以看见我们拿到了包名,以及类名,类名加上“_ViewBinding”就是我们生成的代理文件。

我们来看一下这个编译时生成的代理文件,在app->build->generated->source->apt中需要我们编译之后才会生成。

                               

                                                                         图10.生成的代理文件

这里需要注意一下,在我们的ClassCreatorProxy中generateJavaCode()方法中的builder.append(“import com.hikvision.lg.demo.*\n”)必须是MainActivity所在的包,不然会报找不到这个包,因为在这个代理类中我们需要传入MainActivity的实例。

提供外部使用

最后我们需要编一个提供给外部使用的api,我们先看一下这个提供外部使用的工具类。

                                  

                                                                                       图11.提供给外部使用的类

这里是使用到了反射,因为是编译时期生成的,所以是不会影响性能的,我们看到先是获取到传入的类的类名MainActivity再加上“_ViewBinding”找到我们生成的代理文件,然后调用代理文件中bind的方法,并且调用该方法完成绑定操作。

最后我们看一下在外部是怎么调用的注解:

                                         

                                                                                      图12.注解的使用

外部使用注解就很简单,只需要先绑定一下所在的类,然后调用注解使用即可。

案例总结

本文通过具体的实例来描述了如何编写一个基于编译时注解的项目,主要步骤为:项目结构的划分、注解模块的实现、注解处理器的编写以及对外公布的API模块的编写。用注解处理器的好处就是可以自动帮我们生成一些重复大量的代码,并且能让我们的类变得干净、逻辑清晰。

Demo GitHub地址:https://github.com/lg2179/AnnotationDemo

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值