注解处理器
注解强大的地方在于: 我们可以在运行时或者编译时处理注解. 在编译时或者运行时处理注解的机制都可以称为一个注解处理器.
注解处理器的类型
注解处理器的类型分为两大类:
运行时处理器: 这种机制是在程序运行时利用
反射
机制去处理注解.编译时处理器: 这种机制是在程序编译时利用
javac
提供的一个apt
工具来处理注解.
运行时处理器的特点: 该机制基于反射机制, 因此灵活性很大, 但是却相对耗性能. 编译时处理器的特点:这种方法是在编译时处理注解, 因此不存在性能的问题, 但是灵活性也相对比较低.
在已有的开源库实现中, 普遍是结合编译时处理器
和反射机制
这两种方法. 举个例子: ButterKnife
. 它是一个View
注入的库. 内部实现的原理: 编译时利用apt
生成findViewById
等一些列模板代码, 然后在运行时利用反射去实例生成的模板代码类, 这样完成了一次注入. 从这里可以看出, ButterKnife
并没有完全基于编译时注解处理器. 而是加了反射.
这样做的好处: 开发者不需要手动实例apt
生成的类. 换句话说: 开发者不需要了解生成类的命令规则. 也许你可能会觉得利用反射会耗性能. 但是仔细想想, 如果没有利用反射的话, 开发者都需要手动编译, 然后再实例化生成的类. 这个过程也是挺繁琐的. 而且, 库内部是有做相应的缓存的, 所以耗的性能还是相对比较低的.
对于反射的利用, 应该适当使用, 而不是避而不用或者滥用.
接下来, 讲讲APT
的一些知识, 最后再仿写一个简单的ButterKnife
作为实战.
AbstractProcessor
开发注解处理器的第一个步骤就是继承AbstractProcessor
. 然后重写其四个方法:
public synchronized void init(ProcessingEnvironment processingEnvironment)
: 这个方法一般是做一些初始化的工作.public SourceVersion getSupportedSourceVersion()
: 该处理器所支持的JDK
版本, 一般是支持到最新:SourceVersion.latestSupported()
.public Set<String> getSupportedAnnotationTypes()
: 你所要处理的注解的类型.public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
: 处理注解的方法. 这个是我们主要实现的方法. 返回值表示当前这个注解类型是否需要被随后的注解处理器处理.true
表示不需要,false
表示后面的注解处理器可能会对本次处理的注解.
认识这4个方法后, 我们还需要熟悉一下编写处理器涉及到的一些概念和API
.
ProcessingEnvironment
ProcessingEnvironment
是在init
方法传入的. 它表示APT
框架的一个处理时上下文. 这个类主要是提供一些工具类. 详细看下面代码注释:
|
|
RoundEnvironment
当APT
启动时, 它将会扫描源文件, 然后进入process
方法处理, 如果这个过程生成了新的文件的话, 新文件会被APT
再次作为输入, 接着process
会被apt
再次调用, 如此循环下去, 直到没有新的文件产生. RoundEnvironment
为处理轮次的上下文环境. 下面是RoundEnvironment
的方法简单介绍:
|
|
Element
Element
代表Java
静态语言结构的元素. 这样听起来可能有点难以理解, 不过看下面的例子就会很好理解了:
|
|
TypeElement
: 代表类或接口元素VariableElement
: 代表字段, 枚举常量, 方法或构造函数的参数, 局部变量, 资源变量和异常参数.PackageElement
: 代表包元素ExecutableElement
: 代表的方法, 构造方法,和接口或者类的初始化块.
DeclaredType, TypeElement和TypeMirror
TypeElement
表示类或接口元素, 可以从中获取类名, 但是不能获得类本身的信息, 比如父类.
TypeMirror
: 表示Java
语言中的类型, 其中包括: 基本类型, 声明类型(类类型和接口类型), 数组类型, 类型变量和空类型. 也代表通配类型参数,可执行文件的签名和返回类型.
DeclaredType
: 代表声明的类型, 类类型还是接口类型,当然也包括参数化类型,比如Set
开发一个注解处理器
掌握了上面的概念后, 我们开始来开发一个仿ButterKnife
的注解处理器.
步骤
继承
AbstractProcessor
并重写其中的四个方法.注册注解处理器.
继承AbstractProcessor
|
|
在getSupportedAnnotationTypes()
中, 我们直接返回了需要处理的注解的全限定名称集合.
注册处理器
写好的处理器还需要处理告诉Javac
, 让它编译时运行我们的处理器. 注册的步骤为: 在项目新建一个resources
目录, 然后在resources
内新建这样的目录结构: /META-INF/services/
. 最后在services
中新建文件: javax.annotation.processing.Processor
. 并且在文件上填写你的处理器的全路径名称. 例如: com.desperado.processors.ViewBindingProcessor
.
其实还有一种更为简单的方法. 添加com.google.auto.service:auto-service:1.0-rc2
这个库(Google开源的). 然后在你的处理器上添加注解: @AutoService(Processor.class)
. 这样, 编译时就会自动帮我们生成注册文件.
如何组织处理器结构
为了不将注解处理器的代码打包进APK
. 我们需要将注解和注解处理器分开. 具体的组织如下:
annotations
:annotations
是一个java lib
. 是我们自定义的注解processors
:processors
也是一个java lib
. 是我们的注解处理器viewbinding
: 为提供为app
的api.
annotations
首先我们在annotation
模块定义一个注解
|
|
processors
在讲处理器模块前, 我们先看看期望生成的代码模板:
|
|
生成的代码的类名为: XXXX$ViewBinding
然后在构造函数中对传入的activity
中的View
进行赋值.
定义处理规则.
BindView
只能标注字段.BindView
不能标注接口中的字段, 只能标注类中的字段.BindView
不能标注抽象类中的字段.BindView
不能标注被private
,static
或者final
修饰的字段BindView
标注字段所属的类必须是Activity
的子类.BindView
标注的字段必须是View
的子类
定义好这些规则后, 我们就可以来看处理器的代码了.
处理过程
|
|
为了使代码结构更为清晰, 这里将注解标注的字段的信息抽象为ViewField
类, 字段所属的类的信息抽象为ViewClass
.
|
|
处理过程其实并不复杂, 只是需要我们熟悉API
而已, 因此, 接下来总结一下获取一些常用信息的方法:
常用的api
|
|
viewbinding
模板代码已经生成了, 但是我们还需要实例化这些类. 这个工作交由viewbinding
. 它提供一些绑定的API
给app
. 这样我们就不需要手动调用了.
实现的思路: 运行时利用反射去实例化对应的模板类. 为了降低反射的消耗, 内部会做响应的缓存操作.
|
|
使用
注解处理器现在算是开发好了. 最后的步骤就是在app
中添加依赖, 然后实现绑定.
在app
的build.gradle
添加下面的依赖:
|
|
然后在MainActivity
中实现注入:
|
|
完…
参考资料