注解是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
dependencies {
...
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
//Module:app 的 build.gradle
dependencies {
...
compile 'com.jakewharton:butterknife:8.4.0'
apt 'com.jakewharton:butterknife-compiler:8.4.0'
}
关于 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)
public class HomeActivity extends BaseActivity {
...
}
注解的解析就在BaseActivity中。我们看一下BaseActivity代码public class BaseActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//注解解析
for (Class c = this.getClass(); c != Context.class; c = c.getSuperclass()) {
ContentView annotation = (ContentView) c.getAnnotation(ContentView.class);
if (annotation != null) {
try {
this.setContentView(annotation.value());
} catch (RuntimeException e) {
e.printStackTrace();
}
return;
}
}
}
解释下上面的代码:
- 第一步:遍历所有的子类
- 第二步:找到修饰了注解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()方法,代码如下: ({
//该注解处理器支持的所有注解全名
})
(SourceVersion.RELEASE_7)
public class ContentViewProcessor extends AbstractProcessor {
...
}
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源码
private void error(Element element, String message, Object... args) {
if (args.length > 0) {
message = String.format(message, args);
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);
}
注册注解处理器
为了让javac -processor能够对定义好的注解处理进行处理,我们需要将注解处理器打包到一个jar文件中,同时,需要在jar文件中增加一个名为javax.annotation.processing.processor的文件来指明jar文件中有哪些注解处理器,这个文件最终目录在jar文件根目录的META-INF/service目录中,jar文件解压后的目录结构如下图:
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完成如下图的工作内容。
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/