注解,也被称为元数据,为我们在代码中添加信息提供了一种形式化的方式,使我们可以在某个时刻可以非常方便地使用这些数据。很多文章都是讲述java注解的,而且很多例子虽然有和Android互通的部分,但是Android开发中也扩展了很多单纯Java中没有的注解。所以这里主要介绍Android开发中的注解,当然包括Java注解。
目前很多框架开发或者Android开发中都用到了注解,SDK开发中也有很多可以对接口添加限制以规范用户使用的规则,这些都是值得我们去学习的。
一、Java注解
1. 什么是Java注解?
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种机制。 Java 语言中的类、方法、变量、参数和包等都可以被注解。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成 class 类文件时,注解可以被嵌入到字节码中。JVM可以保留注解内容,在运行时可以获取到注解内容 。 当然它也支持自定义 Java 注解。
2. Java中内置的标准注解
- @Override,表示当前类中的方法定义,将覆盖超类中的方法定义。
- @Deprecated,被此注解标记的元素表示被废弃,如果程序员在某个元素上使用了该注解,那么编译器会发出警告。
- @SuppressWarnings,关闭不当的编译器警告信息。
如下为这几个注解的实现,可以看到,除了@符号的使用以外,它基本与Java固有语法一致。只是注解上又增加了元注解。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
3. 什么是元注解?
有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。
元注解共有四种:
元注解 | 说明 | 取值 |
---|---|---|
@Target | 表示该注解可以用在什么地方 | ElementType.ANNOTATION_TYPE 可以应用于注释类型。 ElementType.CONSTRUCTOR 可以应用于构造函数。 ElementType.FIELD 可以应用于字段或属性。ElementType.LOCAL_VARIABLE 可以应用于局部变量。 ElementType.METHOD 可以应用于方法。 ElementType.PACKAGE 可以应用于包声明。 ElementType.PARAMETER 可以应用于方法中的参数。ElementType.TYPE 可以应用于类或接口的任何元素。 |
@Retention | 表示需要在什么级别保存该注解信息 | 1.SOURCE:在源文件中有效(即源文件保留) 2.CLASS:在class文件中有效(即class保留) 3.RUNTIME:在运行时有效(即运行时保留) |
@Documented | 表示将此注解包含在Javadoc中 | 无 |
@Inherited | 表示允许子类继承父类中的注解 | 无 |
经过对元注解的表格进行理解,可以回头看我们平常使用的标准注解Override 、Deprecated 和SuppressWarnings 。可以看到他们分别都由元注解进行了注释,并且我们能够明白,为啥@Override只能用在方法上(@Target),而不能用在类、构造函数、变量上,而@Deprecated就可以用在构造函数、变量、方法、包、参数等,因为其代表的意义就可以表示它能够标注的所有类型都是废弃的元素。
元注解的使用
@Target:用以定义 Annotation 能够被应用于源码的那些位置;它定义的 value 值是一个 ElementType[] 数组,只有一个元素时,可以省略数组的写法。
// @Target 只有一个元素时的情况
@Target(ElementType.METHOD)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
// @Target 有多个元素的情况
@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface Report {
...
}
@Retention:用以定义 Annotation 的生命周期;如果未使用@Retention元注解,则该 Annotation 默认为 CLASS 阶段。因为通常我们自定义的Annotation
都是RUNTIME
,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)
这个元注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Repeatable:用以定义 Annotation 是否可重复; 经过 @Repeatable 修饰后的注解,在某个类型声明处,可以添加多个该注解:
@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
@Inherited:用以定义子类是否可以继承父类中定义的 Annotation。@Inherited
仅针对@Target(ElementType.TYPE)
类型的annotation
有效,并且仅针对class
的继承,对interface
的继承无效:
//定义注解
@Inherited
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
//父类中使用
@Report(type=1)
public class Person {
}
//子类中默认也定义了 @Report 注解
public class Student extends Person {
}
4. 注解的三种类型 (三个阶段)
接下来,我们来对Android程序的三个阶段进行解释,从而解释三种类型的注解。
-
RetentionPolicy.
SOURCE
类型的注解在编译期之前有效,在编译之后就会被编译器丢掉; -
RetentionPolicy.
CLASS
类型的注解仅保存在.class文件中,它们不会被加载进JVM,这类注解只被一些底层库使用,一般我们不必自己处理。 -
RetentionPolicy.
RUNTIME
类型的注解会被加载进JVM,并且在运行期可以被程序读取。这也是最常用的注解。
对这三种类型做进一步解释:
我们在as或者idea等IDE开发时的Java源码时期,即SOURCE阶段。然后经过gradle或其他构件工具编译,变成了.Class 文件,在 Android apk 中的 dex 文件中都是 .class 文件,此时即Retention的Class阶段。之后便是程序运行阶段,变成Jvm运行的RUNTIME阶段,此时应用是执行状态,如果定义注解为RUNTIME,则此时的注解是保留的。
从上述表格可以了解,source 即源文件保留,即是 .java 文件中保留。Runtime 即运行时有效,即在Android应用在运行时仍然存在。
5. 使用注解的框架
5.1 JUnit:
是一个Java语言的单元测试框架,可以大大缩短你的测试时间和准确度。
@Test:进行测试的方法 。
@BeforeClass : 该方法表示启动测试类对象测试之前启动的方法, 所以该方法必须是static 修饰的(可以通过类名直接访问).一般用来打开配置文件,初始化资源等
@AfterClass :该方法表示测试类对象测试完成之后启动的方法, 所以该方法必须是static 修饰的(可以通过类名直接访问).一般用来关闭数据库,结束资源等
@Before :该方法表示调用每个测试方法前都会被调用一次
@After :该方法表示调用每个测试方法后都会被调用一次
@Ignore :已经被忽略的测试方法 ,我们测试的话,会自动过滤掉
……
5.2 ButterKnife :
Android 开发中 IOC 框架,它减少了大量重复的代码。专注于Android系统的View注入框架,以前总是要写很多findViewById来找到View对象,有了ButterKnife可以很轻松的省去这些步骤。并且使用ButterKnife对性能基本没有损失,因为ButterKnife用到的注解并不是在运行时反射的,而是在编译的时候生成新的class。
ButterKnife项目地址:https://github.com/JakeWharton/butterknife有需要研究的同学可以通过阅读源码深入学习。
涉及注解有:
@BindView
@OnClick
@BindString等。
5.3 Dagger2
著名的依赖注入框架,依赖注入,顾名思义,就是说当代码执行过程中需要某个服务对象的时候,不是通过当前代码自己去构造或者去查找获取服务对象,而是通过外部将这个服务对象传给当前代码。
其自定义的注解有:
@Inject
@Moudle
@Provider
@Component
@Singleton
@Scope
@Qualifier
@Named
对相关知识需要进一步学习的可以参考项目地址: https://github.com/google/dagger
5.4 Retrofit
Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装,网络请求的工作本质上是 OkHttp 完成,而 Retrofit 仅负责 网络请求接口的封装。Retrofit通过注解的方式,进行网络请求描述。
其常用注解有:
@GET
@POST
@PUT
@DELETE
@PATCH
@HEAD
@OPTIONS
@HTTP
需要继续深入了解的同学可以参考项目地址:https://github.com/square/retrofit
5.5 EventBus
EventBus是一种用于Android的事件发布-订阅总线,由GreenRobot开发,它简化了应用程序内各个组件之间进行通信的复杂度,尤其是碎片之间进行通信的问题,可以避免由于使用广播通信而带来的诸多不便。
其主要的注解为
@Subscribe
项目地址为:https://github.com/greenrobot/EventBus
5.6 Arouter
ARouter 是阿里开源的组件化框架,或者叫做路由框架。可以用来映射页面关系,实现跳转相关的功能。在 Android 中,常被用来进行组件化通讯。
常用注解有:
@Route
@Interceptor
@Autowired等。
项目地址为:https://github.com/alibaba/ARouter
6. 自定义注解类
6.1 定义注解
注解的定义,是通过 @interface 语法来实现的。
定义注解参数说明:
1. 注解的参数类似无参数方法;
2. 强烈推荐用 default 设置默认值;
3. 最常用的参数应当命名为 value。
代码举例说明:
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
//用元注解来配置自定义的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
6.2 注解参数赋值
定义一个注解时,还涉及定义配置参数:
- 所有基本类型;
- String;
- 枚举类型;
- 基本类型、String、Class以及枚举的数组。
注意规则事项:
- 由于配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。
- 注解的配置参数可以有默认值 default,缺少某个配置参数时将使用默认值。
- 定义注解基本都会有一个名为 value 的配置参数,如果参数名称是
value
,且只有一个参数,那么可以省略参数名称。 - 如果只写注解,相当于全部使用默认值
6.3 处理注解
因为注解定义后也是一种class
,所有的注解都继承自java.lang.annotation.Annotation
,因此,读取注解需要使用到Java反射API。
Java反射中提供的读取Annotation的方法如下:
判断某个注解是否存在于Class
、Field
、Method
或Constructor
:
Class.isAnnotationPresent(Class)
Field.isAnnotationPresent(Class)
Method.isAnnotationPresent(Class)
Constructor.isAnnotationPresent(Class)
//例如:
// 判断@Report是否存在于Person类:
Person.class.isAnnotationPresent(Report.class);
使用反射API读取Annotation:
Class.getAnnotation(Class)
Field.getAnnotation(Class)
Method.getAnnotation(Class)
Constructor.getAnnotation(Class)
// 例如:
// 获取Person定义的@Report注解:
Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();
使用反射API读取Annotation的两种方式:
// 方法一: 先判断 Annotation 是否存在,如果存在,再直接读取
Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
Report report = cls.getAnnotation(Report.class);
...
}
// 第二种方法是直接读取Annotation,如果Annotation不存在,将返回null:
Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
...
}
读取方法中参数的Annotation
读取方法、字段和构造方法的Annotation
和Class类似。但要读取方法参数的Annotation
就比较麻烦一点,因为方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解就必须用一个二维数组来表示。例如,对于以下方法定义的注解:
public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}
// 开始读取方法参数的注解
// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
if (anno instanceof Range) { // @Range注解
Range r = (Range) anno;
}
if (anno instanceof NotNull) { // @NotNull注解
NotNull n = (NotNull) anno;
}
}
6.4 实际举例
为了更好的了解注解的作用和框架如何使用注解达到目的,方便用户提高编码效率。这里模仿ButterKnife的功能实现一个注解。首先自定义注解InjectDIYLayout , 这里为了简单示例用了Runtime运行时注解,并采用反射方法调用执行。
//定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectDIYLayout {
int value();
}
// 编写工具类,用来处理注解。在BaseActivity中的onCreate中初始化。InjectDIYUtils.inject(this);
public class InjectDIYUtils {
private static final String TAG = "InjectDIYUtils";
public static void inject(Object context){
injectDIYLayout (context);
}
//之后就是注解的解释了,就是injectLayout的方法体。
//这里通过拿到参数activity的Context,并获取Activity的类,
//之后通过类getAnnotation得到此类注释的注解,经打印可以判断是否此注解。
//然后通过反射获取到setContentView,并用反射方法的invoke调用,传入注解中的layout值。
private static void injectDIYLayout (Object context) {
Class<?> aClass = context.getClass();
InjectDIYLayout annotation = aClass.getAnnotation(InjectDIYLayout.class);
Log.e(TAG, "injectLayout: annotation.value()="+annotation.value());
try {
Method contentView = aClass.getMethod("setContentView", int.class);
try {
Object invoke = contentView.invoke(context, annotation.value());
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
//在Activity中使用注解
@InjectDIYLayout(R.layout.activity_main)
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//注释掉setContentView
//setContentView();
}
}