注解是 Java 语言的特性之一,它是在源代码中插入标签,这些标签在后面的编译或者运行过程中起到某种作用,每个注解都必须通过注解接口 @Interface 进行声明,接口的方法对应着注解的元素。
在上一篇文章 JSR-330 和 assertion(断言) 介绍中介绍了 Java 中的 JSR-330 规范,这个规范就是使用注解的方式。
这篇文章主要介绍注解在 Android 中的应用。
Andorid 中的应用
JSR-330 规范只发布了规范 API 源码,主要是面向依赖注入使用者,而对注入器实现、配置并未作详细要求。
该规范主要配合依赖注入框架来使用。在 Android 中的依赖注入框架有 ButterKnife 和 Dagger2。下面简单分析 ButterKnife 的应用。
关于依赖注入框架的好处,我理解 1. 可以减少样板类代码,比如 Setter 方法。2. 程序运行期间,可以将某种依赖关系动态注入到对象中,实现懒加载(需要的时候才会去加载)。
ButterKnife
ButterKnife 从严格意义上讲不算是依赖注入框架,它只是专注于 Android 系统的 VIew 注入框架,并不支持其他方面的注入。它可以减少大量 findViewById 以及 setOnClickListener 代码。
ButterKnife 用到了编译时注解,因为它需要依赖 android-apt 插件
//project 的 build.gradle |
关于 android-apt 插件后面会介绍。
ButterKnife 提供的注解有:
- 绑定控件:@BindView
- 绑定资源:@BindString、@BindArray、@BindBool、@BindColor、@BindDimen、@BindDrawable、@BindBitmap。
- 绑定监听:@OnClick、@OnLongClick、@OnTextChanged、@OnTouch
- 可选绑定:@Nullable
@Nullable 用于 @BindView 或其他的注解操作符,如果找不到目标时,避免引发异常,例如:
(R.id.tv_title)
TextView tvTitle;
ButterKnife 原理解析
前面提到 ButterKnife 使用的是编译时注解,先看看最常用的 @BindView 注解的源码:
(RetentionPolicy.Class) |
@interface 声明会创建一个实际的 Java 接口,与其他任何接口一样,注解也会编译成. class 文件。@Retention 和 @Target 下面会介绍到。
关于 ButterKnife 更多源码分析,请看这篇文:butterknife 源码分析
Java 注解的分类
Java API 中默认定义的注解叫做标准注解。它们定义在 java.lang、java.lang.annotation 和 javax.annotation 包中。按照使用场景不同,可以分为如下三类:
编译相关注解
编译相关的注解是给编译器使用的,有以下几种:
- @Override:编译器检查被注解的方法是否真的重载了一个来自父类的方法,如果没有,编译器会给出错误提示。
- @Deprecated:可以用来修饰任何不再鼓励使用或已被弃用的属性、方法等。
- @SuppressWarnings:可用于除了包之外的其他声明项中,用来抑制某种类型的警告。
- @SafeVarargs:用于方法和构造函数,用来断言不定长参数可以安全使用
- @Generated:一般是给代码生成工具使用,用来表示这段代码不是开发者手动编写的,而是工具生成的。被 @Generated 修饰的代码一般不建议手动修改它。
- @FunctionalInterface:用来修饰接口,表示对应得接口是带单个方法的函数式接口
资源相关注解
一共有四个,一帮用在 JavaEE 领域,Android 开发中应该不会用到,就不在详细介绍了。
分别是:
- @PostConstruct
- @PreDestroy
- @Resource
- @Resources
元注解
Butterknife 的 Bind 注解用到的就是元注解。
元注解,顾名思义,就是用来定义和实现注解的注解,总共有如下五种:
- @Retention, 用来指明注解的访问范围,也就是在什么级别保留注解,有三种选择:
- 源码级注解:使用 @Retention(RetentionPolicy.SOURCE) 修饰的注解,该类型修饰的注解信息只会保留在 .java 源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的 .class 文件中。
- 编译时注解:使用 @Retention(RetentionPolicy.CLASS) 修饰的注解,该类型的注解信息会保留在 .java 源码里和 .class 文件里,在执行的时候会被 Java 虚拟机丢弃,不会加载到虚拟机中。
- 运行时注解:使用 @Retention(RetentionPolicy.RUNTIME) 修饰的注解,Java 虚拟机在运行期间也保留注解信息,可以通过反射机制读取注解的信息
未指定类型时,默认是CLASS类型。
- @Target, 这个注解的取值是一个 ElementType 类型的数组,用来指定注解所使用的对象范围,共有十种不同的类型,如下表所示,同时支持多种类型共存,可以进行灵活的组合。
元素类型 | 适用于 |
---|---|
ANNOTATION_TYPE | 注解类型声明 |
CONSTRUCTOR | 构造函数 |
FIELD | 实例变量 |
LOCAL_VARIABLE | 局部变量 |
METHOD | 方法 |
PACKAGE | 包 |
PARAMETER | 方法参数或者构造函数的参数 |
TYPE | 类(包含 enum)和接口(包含注解类型) |
TYPE_PARAMETER | 类型参数 |
TYPE_USE | 类型的用途 |
如果一个注解的定义没有使用 @Target 修饰,那么它可以用在除了 TYPE_USE 和 TYPE_PARAMETER 之外的其他类型声明中
- @Inherited, 表示该注解可以被子类继承的。
- @Documented, 表示被修饰的注解应该被包含在被注解项的文档中(例如用 JavaDoc 生成的文档)
- @Repeatable, 表示这个注解可以在同一个项上面应用多次。不过这个注解是在 Java 8 中才引入的,前面四个元注解都是在 Java 5 中就已经引入。
运行时注解
前面说过,要定义运行时注解只需要在声明注解时指定 @Retention(RetentionPolicy.RUNTIME) 即可,运行时注解一般和反射机制配合使用。
熟悉 java 反射机制的同学一定对 java.lang.reflect 包非常熟悉,该包中的所有 api 都支持读取运行时 Annotation 的能力。相比编译时注解性能比较低,但灵活性好,实现起来比较简单。
Butterknife 在较低版本依然是通过运行时反射实现 View 的注入,性能较低下,不过在 8.0.0 版本以后使用编译时注解来提升性能。
运行时注解的简单使用
下面展示一个 Demo。其功能是通过注解实现布局文件的设置。
之前我们是这样设置布局文件的:
|
如果使用注解,我们就可以这样设置布局了
(R.layout.activity_home) |
我们先不讲这两种方式哪个好哪个坏,我们只谈技术不谈需求。
那么这样的注解是怎么实现的呢?很简单,往下看。
- 创建一个注解
(RetentionPolicy.RUNTIME)
({ElementType.TYPE})
public ContentView {
int value();
}
前面已经讲过元注解,这不不再介绍。
- 对于:public @interface ContentView
这里的 interface 并不是说 ContentView 是一个接口。就像申明类用关键字 class。申明枚举用 enum。申明注解用的就是 @interface。
(值得注意的是:在 ElementType 的分类中,class、interface、Annotation、enum 同属一类为 ElementType.Type,并且从官方注解来看,interface 是包含 @interface 的)
/** Class, interface (including annotation type), or enum declaration */ |
- 对于:int value();
返回值表示这个注解里可以存放什么类型值。比如我们是这样使用的
(R.layout.activity_home) |
R.layout.activity_home 实质是一个 int 型 id,如果这样用就会报错:
(“string”) |
关于注解的具体语法,可以看这篇文章 Android 编译时注解框架 - 语法讲解
注解解析
注解申明好了,但具体是怎么识别这个注解并使用的呢?
(R.layout.activity_home) |
注解的解析就在 BaseActivity 中。我们看一下 BaseActivity 代码
public class BaseActivity extends AppCompatActivity { |
解释下上面的代码:
- 第一步:遍历所有的子类
- 第二步:找到修饰了注解 ContentView 的类
- 第三步:获取 ContentView 的属性值。
- 第四步:为 Activity 设置布局。
总结:要定义运行时注解,只需要在声明注解时指定 @Retention(RetentionPolicy.RUNTIME) 即可,运行时注解一般和反射机制配合使用,相比编译时注解性能比较低,但实现比较简单,会提高一定的开发效率。
编译时注解
编译时注解能够自动处理 Java 源文件并生成更多的源码、配置文件、脚本或其他可能想要生成的东西。这些操作是通过注解处理器(Annotation Processor Tool)完成的。Java 通过在编译期间调用 javac -processor 命令可以调起注解处理器,它能够实现编译时注解的功能。
注解处理器其实是在 javac 开始编译之前,以 java 源码文件或编译后的 class 文件作为输入,然后输出另一些文件,可以是. java 文件,也可以是. class 文件,但通常我们输出的是. java 文件,这些. java 文件回合其他源码文件一起被 javac 编译,从而提高函数库的性能。
定义注解处理器
自定义编译时注解后,需要编写 Processor 类实现注解处理器,处理自定义注解。Processor 继承自 AbstractProcessor 类并实现 process 方法,同时需要指定注解处理器能够处理的注解类型以及支持的 Java 版本,语句如下:
public class JsonAnnotationProcessor extends AbstractProcessor { |
一个注解处理器,只能产生新的源文件,它不能够修改一个已经存在的源文件。当没有属于该 Process 处理的注解被使用时,process 不会执行。
从 Java7 开始,我们也可以使用注解来代替上面的 getSupportedAnnotationTypes() 和 getSupportedSourceVersion() 方法,代码如下:
({ |
Element 类型
所有通过注解取得元素都将以 Element 类型等待处理,也可以理解为 Element 的子类类型与自定义注解时用到的 @Target 是有对应关系的。
Element 的官方注释:Represents a program element such as a package, class, or method.
Each element represents a static, language-level construct (and not, for example, a runtime construct of the virtual machine).
表示一个程序元素,比如包、类或者方法。
Element 的子类有:
ExecutableElement
表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。对应 @Target(ElementType.METHOD) @Target(ElementType.CONSTRUCTOR)PackageElement
表示一个包程序元素。提供对有关包极其成员的信息访问。对应 @Target(ElementType.PACKAGE)TypeElement
表示一个类或接口程序元素。提供对有关类型极其成员的信息访问。
对应 @Target(ElementType.TYPE)
注意:枚举类型是一种类,而注解类型是一种接口。
TypeParameterElement
表示一般类、接口、方法或构造方法元素的类型参数。
对应 @Target(ElementType.PARAMETER)VariableElement
表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
对应 @Target(ElementType.LOCAL_VARIABLE)
Processor 输出日志
虽然是编译时执行 Processor, 但也是可以输入日志信息用于调试的。Processor 日志输出的位置在编译器下方的 Messages 窗口中。Processor 支持最基础的 System.out 方法。
同样 Processor 也有自己的 Log 输出工具: Messager。
//同样是Butterknife源码 |
注册注解处理器
为了让 javac -processor 能够对定义好的注解处理进行处理,我们需要将注解处理器打包到一个 jar 文件中,同时,需要在 jar 文件中增加一个名为 javax.annotation.processing.processor 的文件来指明 jar 文件中有哪些注解处理器,这个文件最终目录在 jar 文件根目录的 META-INF/service 目录中,jar 文件解压后的目录结构如下图:
图片来自http://blog.csdn.net/lmj623565791/article/details/43452969
javax.annotation.processing.Processor 文件的内容是注解处理器全路径名,如果存在多个注解处理器,以换行进行分隔,代码看图片
源文件的目录是,我们需要在 src/main/java 同级目录中新建一个名为 resources 的目录,将 META-INF/services/javax.annotation.processing.Processor 文件放进去就行
注意,注解处理器所在的 Android Studio 工程必须是 Java Library 类型,而不应该是 Android Library 类型。因为 Android Library 的 JDK 中不包含某些 javax 包里面的类。
手动实现上面注册过程很繁琐,因此 Google 开源了一个名为 AutoService 的函数库,使用这个库后,只需在自定义 Processor 时使用 @AutoService 注解标记即可完成上面注册步骤。
(Processor.class) |
android-apt 插件
注解处理器所在的 jar 文件只能在编译期间起作用,到应用运行时不会用到,因此,在 build.gradle 中引入依赖时应该以 provided 方式,而不是 compile 方式引入。
当然,我们可以使用 android-apt 插件的方式。
android-apt 是由一位开发者自己开发的 apt 框架,源代码托管在这里,随着 Android Gradle 插件 2.2 版本的发布,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt ,自此 android-apt 作者在官网发表声明最新的 Android Gradle 插件现在已经支持 annotationProcessor,并警告和或阻止 android-apt ,并推荐大家使用 Android 官方插件 annotationProcessor。
但是很多项目目前还是使用 android-apt,如果想替换为 annotationProcessor,那就要知道 android-apt 是如何使用的。
它的作用主要如下:
- 只在编译期间引入注解处理器所在的函数库作为依赖,不会打包到最终生成的 APK 中。
- 为注解处理器生成的源码设置好正确的路径,以便 Android Studio 能够正常找到,避免报错。
Project 项目中使用 android-apt 插件
1. 使用该插件,添加如下到你的构建脚本中:
//配置在Project下的build.gradle中
buildscript {
repositories {
mavenCentral()
}
dependencies {
...
//替换成最新android-apt版本
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.neenbedankt.android-apt'2. 接着以 apt 的方式引入注解处理器函数库作为依赖
dependencies {
apt'com.bluelinelabs:logansquare-compiler:1.3.6'
compile 'com.bluelinelabs:logansquare:1.3.6'
}
LoganSquare 是一个实现了编译时注解以提高性能的 JSON 解析函数库。
通常在使用的时候,项目依赖可能分为多个部分。上面的 compiler 库就有两个组件 loganSquare-compiler 和 loganSquare。loganSquare-commpiler 仅用于编译时,是 loganSquare 的注解处理器,运行时必需使用 loganSquare。
基本使用就是上面这两点,想用 annotationProcessor 替代 android-apt。删除和替换相应部分即可
Provided 和 apt/annotationProcessor 区别
provided vs apt 使用注解处理器的不同?
- provided 将会导入注解处理器的 classes 和它的依赖到 IDE 的类路径下。这意味着你可以附带的引入并使用这些 classes。例如,当注解处理器使用 Guava,你可能错误的 import 其相关代码到你的 Android 代码中。当运行时将导致 crash。
- provided 也可以用在重复引用的库上,避免依赖重复的资源。
- 使用 apt,注解处理器的 classes 将不会添加到你当前的类路径下,仅仅用于注解处理过程。并且会把所有注解处理器生成的 source 放在 IDE 的类路径下,方便 Android Studio 引用。
具体可以参考:深入理解编译注解(三)依赖关系 apt/annotationProcessor 与 Provided 的区别
APT 处理 annotation 的流程
越来越多第三方库使用 apt 技术,如 DBflow、Dagger2、ButterKnife、ActivityRouter、AptPreferences。在编译时根据 Annotation 生成了相关的代码,非常高大上但是也非常简单的技术,可以给开发带来了很大的便利。
注解处理器(AbstractProcess)+ 代码处理(javaPoet)+ 处理器注册(AutoService)+apt
具体流程:
- 1. 定义注解(如 @inject)
- 2. 定义注解处理器
- 3. 在处理器里面完成处理方式,通常是生成 Java 代码。
- 4. 注册处理器
- 5. 利用 APT 完成如下图的工作内容。
图片来自http://blog.csdn.net/xx326664162/article/details/68490059
annotationProcessor 介绍
annotationProcessor 是 APT 工具中的一种,他是 google 开发的内置框架,不需要引入,可以直接在 build.gradle 文件中使用,
ButterKnife 就是使用 annotationProcessor 处理注解,如下:
dependencies { |
apt vs annotationProcessor 两者有何不同?
android-apt 是由一位开发者自己开发的 apt 框架,源代码托管在这里,随着 Android Gradle 插件 2.2 版本的发布,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt ,自此 android-apt 作者在官网发表声明最新的 Android Gradle 插件现在已经支持 annotationProcessor,并警告和或阻止 android-apt ,并推荐大家使用 Android 官方插件 annotationProcessor。
最近 Android N 的发布,android 迎来了 Java 8,要想使用 Java 8 的话必须使用 Jack 编译,android-apt 只支持 javac 编译而 annotationProcessor 既支持 javac 同时也支持 jack 编译。
想用 annotationProcessor 替代 android-apt。删除和替换相应部分即可,具体可以参考这篇文章
文章参考:
Android 打造编译时注解解析框架 这只是一个开始
Android APT(编译时代码生成)最佳实践
Android 编译时注解框架系列 1 - 什么是编译时注解
你必须知道的 APT、annotationProcessor、android-apt、Provided、自定义注解
《Android 高级进阶》一书——注解在 Android 中的应用
本文链接:http://agehua.github.io/2017/04/10/Annotation-Android-usage/