Android自定义注解处理器详解

Android自定义注解处理器详解

2023年6月28日发(作者:)

Android⾃定义注解处理器详解Demo GitHub地址:案例简述在我们Android项⽬中很多第三⽅库都⽤到了注解,像我们项⽬中最常⽤的butterKnife以及eventBusy以及Retrofit都是以注解为基础进⾏使⽤的,通过使⽤注解能很⼤程度上节省代码,但同样注解也很容易让初学者产⽣困惑,特别是在使⽤这类项⽬的时候,有时候出现错误会难以调试,主要原因还是很多⼈并不了解这类框架其内部的原理,所以遇到问题时会消耗⼤量的时间去排查。其次我们如果有好的想法,发现某些代码需要重复创建,我们也可以⾃⼰来写个框架⽅便⾃⼰⽇常的编码,提升编码效率;最后也算是⾃⾝技术的提升,本⽂我们介绍⼀下注解的原理以及使⽤过程,以及⾃⼰编写⼀个View注⼊的框架来减少我们编写页⾯时重复使⽤的findViewById()⽅法;概念介绍1.注解定义 注解(Annotation),也叫元数据。⼀种代码级别的说明。它是JDK1.5及以后版本引⼊的⼀个特性,与类、接⼝、枚举是在同⼀个层次。它可以声明在包、类、字段、⽅法、局部变量、⽅法参数等的前⾯,⽤来对这些元素进⾏说明,注释。初学者可以这样理解注解:想像代码具有⽣命,注解就是对于代码中某些鲜活个体的贴上去的⼀张标签。简化来讲,注解如同⼀张标签。2.注解作⽤ 1、⽣成⽂档。这是最常见的,也是java 最早提供的注解。常⽤的有@param @return 等 2、跟踪代码依赖性,实现替代配置⽂件功能。⽐如Dagger 2依赖注⼊,未来java开发,将⼤量注解配置,具有很⼤⽤处; 3、在编译时进⾏格式检查。如@override 放在⽅法前,如果你这个⽅法并不是覆盖了超类⽅法,则编译时就能检查出。3.元注解 tion提供了四种元注解,专门注解其他的注解(在⾃定义注解的时候需要使⽤到元注解): (1) 注解是否将包含在JavaDoc中 (2)@Retention 什么时候使⽤该注解,:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,所以他们不会写⼊字节码,像@overrid这类注解。:在类加载的时候被丢弃,在字节码⽂件的处理中有⽤,注解默认使⽤这种⽅式,E:始终不会丢弃,运⾏期也保留该注 解,因此可以使⽤反射机制读取该注解的信息。运⾏时期使⽤反射可能会影响性能。 (3)@Target 表⽰该注解⽤于什么地⽅,默认值为任何元素,表⽰该注解⽤于任何地⽅,可⽤的ElementType参数包括: UCTOR:⽤于描述构造器 :成员变量,对象,属性(包括enum实例)

_VARIBALE:⽤于描述局部变量 :⽤于描述⽅法 E:⽤于描述包 ER:⽤于描述参数 :⽤于描述类,接⼝(包括注解类型)活enumeration声明 (4)@Inherited 定义该注解和⼦类的关系,如果⼀个使⽤了@Inherited修饰的annotation类型被⽤于⼀个class,则这个annotation将被⽤于该class的⼦类

4.⾃定义注解 ⾃定义注解类编写需要注意的⼀些规则:

tion型定义为@interface,所有的Annotation会⾃动继承tion这个接⼝,并且不能再去集成别的类或是接⼝。

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此处为,并且是保留到编译时期,所以使⽤,因为id是int型,并且我们只需要获取到id的值即可,所以使⽤int value()来进⾏设置。⽅法详解编写完注解类之后就需要编写注解处理器来处理该注解,这⾥是⽐较复杂的,讲解之前需要先导⼊⼀个auto-service的第三⽅库,如果不使⽤这个库也可以,但是需要⼿动去META-INF中编写⽂件对注解进⾏设置。

图3.导⼊依赖如果我们不使⽤auto-service第三⽅库就需要⼿动编写下图这个⽂件。

图-INF⽂件 我们编写的是,不会对性能有任何影响的:编译时注解。也有⼈叫它代码⽣成,其实他们还是有些区别的,在编译时对注解做处理,通过注解,获取必要信息,在项⽬中⽣成代码,运⾏时调⽤,和直接运⾏⼿写代码没有任何区别。⽽更准确的叫法:APT - AnnotationProcessing Tool。 ⾸先我们了解⼀下注解处理器的基本使⽤⽅法:⾃定义注解处理器必须继承AbstractProcessor抽象类,这个类有⼀个重要的⽅法process();除此之外还有三个⽅法需要我们关注包括init(),getSupportedSpurceVersion()以及getSupportedAnnotationTypes();我们先看process()⽅法所有的注解处理都是从process()⽅法开始,你可以理解为,当APT找到所有需要处理的注解后,会回调这个⽅法,通过这个⽅法的参数,能够拿到所需要的信息。这个⽅法有两个参数我们先看⼀下。

图s()⽅法参数参数Set 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,⼀般这⾥有四个⽅法是固定的格式,我们看⼀下代码:

图sor代码在实现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我们先来看⼀下收集信息的的过程:收集信息:

图7.收集信息⽂中对每⼀⾏代码的注释已经解释的很清楚了,我们简单介绍⼀下这个过程,⾸先我们会调⽤⼀下();因为process可能会多次调⽤所以为了避免⽣成重复的代理类,我们会先清空⼀下这个存放代理类的集合,然后通过mentAnnotatedWith()拿到我们通过@BindView注解的元素,这⾥返回值因为我们提前知道是⽤于变量上的所以就是VariableElement集合。接下来for循环我们的元素,然后拿到对应的类信息TypeElement,继⽽⽣成ClassCreatorProxy对象,这⾥通过⼀个mProxyMap进⾏检查,key为fullClassName即类的全路径,如果没有⽣成才会去⽣成⼀个新的,ClassCreatorProxy与类是⼀⼀对应。接下来,会将与该类对应的且被@BindView声明的VariableElement加⼊到ClassCreatorProxy中去,key为我们声明时填写的Id,即View的id。这样就完成了信息的收集,收集完成信息后,应该就可以去⽣成代理类了。

⽣成代理类

图8.⽣成代理类可以看到⽣成代理类的代码⾮常的简短,主要就是遍历我们的mProxyMap,然后取得每⼀个ClassCreatorProxy,最后通过mFiler来创建⽂件对象,类名为xyClassFullName(),写⼊的内容为teJavaCode()。⽣成Java代码我们看⼀下Java代码⽣成的⽅式。

图代码⽣成⽣成代码这⾥⽐较简单,我们可以看见我们拿到了包名,以及类名,类名加上“_ViewBinding”就是我们⽣成的代理⽂件。我们来看⼀下这个编译时⽣成的代理⽂件,在app->build->generated->source->apt中需要我们编译之后才会⽣成。

图10.⽣成的代理⽂件这⾥需要注意⼀下,在我们的ClassCreatorProxy中generateJavaCode()⽅法中的(“.*n”)必须是MainActivity所在的包,不然会报找不到这个包,因为在这个代理类中我们需要传⼊MainActivity的实例。提供外部使⽤最后我们需要编⼀个提供给外部使⽤的api,我们先看⼀下这个提供外部使⽤的⼯具类。

图11.提供给外部使⽤的类这⾥是使⽤到了反射,因为是编译时期⽣成的,所以是不会影响性能的,我们看到先是获取到传⼊的类的类名MainActivity再加上“_ViewBinding”找到我们⽣成的代理⽂件,然后调⽤代理⽂件中bind的⽅法,并且调⽤该⽅法完成绑定操作。最后我们看⼀下在外部是怎么调⽤的注解:

图12.注解的使⽤外部使⽤注解就很简单,只需要先绑定⼀下所在的类,然后调⽤注解使⽤即可。案例总结本⽂通过具体的实例来描述了如何编写⼀个基于编译时注解的项⽬,主要步骤为:项⽬结构的划分、注解模块的实现、注解处理器的编写以及对外公布的API模块的编写。⽤注解处理器的好处就是可以⾃动帮我们⽣成⼀些重复⼤量的代码,并且能让我们的类变得⼲净、逻辑清晰。

发布者:admin,转转请注明出处:http://www.yc00.com/web/1687954451a60519.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信