标题党一回,实际上这是一个关于注解、反射及简单使用的学习整理。当然,在常用框架:Dagger2、ButterKnife、Retrofit中也确实可以看到注解的身影,注解想必大家是很熟悉的,例如 Java 自带的注解 @Override
、 @Depressed
、@SuppressWarnings
,在日常开发中,使用框架时经常使用到其自定义的注解,注解让框架的使用看起来更简单明了。
以下是一个简单的例子,天气应用中定位当前城市时使用 EasyPermissions 获取定位权限后,执行该方法:
1 | 10010) ( |
EasyPermissions 中有着唯一的一个注解 @AfterPermissionGranted
,它的使用场景也很简单,用它来修饰方法,该方法在声明的权限被用户所授予之后执行。就这样简单的声明之后,就可以达到一种特定的效果。
可以看到注解是用字符 @ 开头,我们使用注解来修饰其后的代码元素,例如字段、方法、方法参数等,通过这种方式,编译器、其他工具可以增强或修改代码的行为。
这就是所谓声明式编程风格。在这种风格中,程序由三个组件组成:
- 声明的关键字和语法本身
- 系统/框架/库,它们负责解释、执行声明式的语句
- 应用程序,使用声明式风格写程序
对应于 Java,这就是注解、反射及其在程序中的使用。其中第三部分在代码中的使用不用多说,只需要根据框架的 API 说明将注解使用在正确的可修饰的内容上即可。下面我们分别看看注解与反射。
注解
注解的创建
我们通过 EasyPermissions 的例子来说明、学习。
1 | (RetentionPolicy.RUNTIME) |
这看起来有点像接口,只是注解在 interface
前加上了 @,另外,可以看到定义注解时使用了两个注解:@Retention
和 @Target
,这两个注解叫做元注解,专门用于定义注解本身。
@Retention
@Retention
表示注解信息保留到什么时候,取值只能有一个,类型为 RetentionPolicy,它是一个枚举,有三个取值:
SOURCE
:只在源代码中保留,编译器将代码编译为字节码文件后就会丢掉CLASS
:保留到字节码文件中,但 Java 虚拟机将 class 文件加载到内存时不一定会在内存中保留RUNTIME
:一直保留到运行时
如果没有声明 @Retention
,默认为 RetentionPolicy.CLASS
。@AfterPermissionGranted
是在运行时进行使用的,所以 @Retention
是 RetentionPolicy.RUNTIME
。而 @Override
和 @SuppressWarnings
都是给编译器用的,所以 @Retention
都是 RetentionPolicy.SOURCE
。事实上,要想用反射去读取注解,这个值一定是要 RUNTIME 的。
@Target
@Target
表示注解的目标,类型 ElementType 是一个枚举,可选值有:
TYPE
:表示类、接口(包括注解),或者枚举声明FIELD
:字段,包括枚举常量METHOD
:方法PARAMETER
:方法中的参数CONSTRUCTOR
:构造方法LOCAL_VARIABLE
:本地变量ANNOTATION_TYPE
:注解类型PACKAGE
:包
如果没有声明@Target,默认为适用于所有类型。
@AfterPermissionGranted
的目标是方法,所以取值 ElementType.METHOD
。
定义元素
@AfterPermissionGranted
的使用中可以看到,其后跟着一个 int 值,这个值是就是注解的元素。如果一个注解没有元素,那这个注解可称为 标记注解,起标记的作用。
注解的元素的定义实际上是通过在定义注解的时候添加一些方法:
- 方法名就是元素的名称
- 方法的返回值就是元素的类型。
其实在使用时,@AfterPermissionGranted(10010)
的完整形式为: @AfterPermissionGranted(value = 10010)
,在元素仅有一个且为 value 时,可以省略“value =”。
注解的元素仅支持有限的类型:基本类型、String、Class、枚举、注解、以及这些类型的数组
注解看起来像接口,而它的元素定义看起来也像接口中的方法,不同的是定义元素时可以指定一个默认值,例如:int value() default 0
,实际上,元素一定要有值,如果定义时没有默认值,那么在使用注解的时候一定要提供具体的值,同时,默认值与提供的值都 不能为 null。由于有此约束,为了表示元素值无效,需要定义默认值为空字符串或者一个负数,这已经是一种惯例。
注解能不能被继承呢?答案是不可以。大家可以自行查阅学习一个与类继承有关的元注解 @Inherited
,这里不展开说明。
通过以上的学习,我们知道如何创建自定义注解,创建之后就可以在程序中使用:修饰指定的目标,提供需要的元素。要注意的是,同一个注解在同一对象上不可以多次使用,当然这种错误 IDE 会提醒你的。
到此,我们完成了 第一步:注解的声明与使用。
但注解的单独使用并不会影响到程序的运行,只是定义和使用注解的话,它和写在代码中的注释一样没太大的作用。要完成完整的功能,就需要对注解进行处理,即需要另一模块来查看代码字段、方法等是否有特殊的注解,找到想要找的注解之后再对相关代码进行解释、执行以达到增加、修改代码的行为的目的。
这就需要学习反射机制。
反射
对于反射机制,大家应该有一个概念,它是一种运行时的机制,在运行时去获取类的相关信息,对其进行一系列的操作:创建、修改、访问、调用等。
由于其生效于运行时,编译器无法在前期检查错误,所以使用反射时要十分注意。另外,反射的性能因为其在访问字段、调用方法前要先查找所以性能有所降低。
如果直接看 EasyPermissons 的反射的例子,会比较简单不系统,所以先了解一下必要的知识后再看例子能更好地理解。
首先,反射的入口为 Class 对象,因此需要先了解 Class 对象。
Class 对象
每一个类都有一个 Class 对象,要在运行时利用反射来操作类的信息,首先要先获取这个 Class 对象。有以下方法可以获取对应的 Class 对象:
- obj.getClass()
- <类名>.class
- Class.forName() 该方法可以获取 Class 对象,同时如果该类还没有加载,就加载它。
补充说明:
- 接口、枚举同样可以使用 2 方式获取对应 Class 对象;
- 基本类型没有 getClass() 方法,也不支持 Class.forName() 的方法进行加载,但其 Class 对象存在,为其包装类型: int.class 为
Class <Integer>
- 数组类型的 Class 对象每一维度都对应一个不同的
- Class.forName() 可能抛出异常
ClassNotFoundException
有了 Class 对象之后,通过一系列方法就可以获取相关的信息,并且可以进行操作字段、调用方法。
由于简单入门学习,这里不会涉及全部相关内容。
想一想一个类的结构,例如:
1 | package com.mupceet.test; |
从上到下我们逐一查看我们关心的一个类包含的信息:
- 类的信息:名称、类的类型(接口、注解、枚举等)、声明信息(修饰符、继承、实现等)
- 构造方法:Contractor
- 字段信息:Field
- 方法信息:Method
类的信息
名称信息、类型信息、声明信息
1 | // 返回 Java 内部使用的真正的名字 |
它们之间的区别可以看以下表格。
getName | getSimpleName | getCanonicalName | getPackage | |
---|---|---|---|---|
int.class | int | int | int | null |
int[].class | [l | int[] | int[] | null |
int[][].class | [[l | int[][] | int[][] | null |
String.class | java.lang.String | String | java.lang.String | java.lang |
String[].class | [Ljava.lang.String; | String[] | java.lang.String[] | null |
java.util.HashMap.class | java.util.HashMap | HashMap | java.util.HashMap | java.util |
java.util.Map.Entry.class | java.util.Map$Entry | Entry | java.util.Map.Entry | java.util |
对于一个给定的 Class 对象,它到底是什么类型呢?可以通过以下方法进行检查:
1 | //是否是数组 |
Class 还可以获取类的声明信息,如修饰符、父类、实现的接口、注解等,如下所示:
1 | //获取修饰符,返回值可通过 Modifier 类进行解读 |
构造方法与创建对象
Class 有一个方法,可以用它来创建对象:
1 | public T newInstance() throws InstantiationException, IllegalAccessException |
它会调用类的默认构造方法(即无参 public 构造方法),如果类没有该构造方法,会抛出异常 InstantiationException
。很多利用反射的库和框架都 默认假定类有无参 public 构造方法,所以使用这些库和框架时要记得提供。
newInstance 只能使用默认构造方法,Class 还有一些方法,可以获取所有的构造方法:
1 | //获取所有的 public 构造方法,返回值可能为长度为 0 的空数组 |
类 Constructor 表示构造方法,通过它可以创建对象,方法为:
1 | public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException |
除了创建对象,Constructor 还有很多方法,可以获取关于构造方法的很多信息,比如:
1 | //获取参数的类型信息 |
Field
类中定义的静态和实例变量都被称为字段,用类 Field 表示。Class 有四个获取字段信息的方法:
1 | //返回所有的 public 字段,包括其父类的,如果没有字段,返回空数组 |
Field 也有很多方法,可以获取字段的信息,也可以通过 Field 访问和操作指定对象中该字段的值,基本方法有:
1 | //获取字段的名称 |
在 get/set 方法中,对于静态变量,obj 被忽略,可以为 null,如果字段值为基本类型,get/set 会自动在基本类型与对应的包装类型间进行转换,对于 private 字段,直接调用 get/set 会抛出非法访问异常 IllegalAccessException,应该先调用 setAccessible(true) 以关闭 Java 的检查机制。
Method
类中定义的静态和实例方法都被称为方法,用类 Method 表示,Class 有四个获取方法信息的方法:
1 | //返回所有的 public 方法,包括其父类的,如果没有方法,返回空数组 |
Method 也有很多方法,可以获取方法的信息,也可以通过 Method 调用对象的方法,基本方法有:
1 | //获取方法的名称 |
对 invoke 方法,如果 Method 为静态方法,obj 被忽略,可以为 null,args 可以为 null,也可以为一个空的数组,方法调用的返回值被包装为 Object 返回,如果实际方法调用抛出异常,异常被包装为 InvocationTargetException 重新抛出,可以通过 getCause 方法得到原异常。
Note:getDeclared* 获取的是仅限于本类的所有的不受访问限制的,而 get* 获取的是包括父类的但仅限于 public 修饰符的
使用反射需要注意的地方
使用反射非常方便,而且在一些特定的场合下可以实现特别的需求,但是使用反射也是需要注意一下几点的:
- 反射最好是使用 public 修饰符的,其他修饰符有一定的兼容性风险,比如这个版本有,另外的版本可能没有
- 大家都知道的 Android 开源代码引起的兼容性的问题,这是 Android 系统开源的最大的问题,特别是那些第三方的 ROM,要慎用。
- 如果大量使用反射,在代码上需要优化封装,不然不好管理,写代码不仅仅是实现功能,还有维护性和可读性方法也需要加强。
EasyPermissons 反射处理
通过以上注解与反射的学习,我们知道注解提升了 Java 语言的表达能力,有效地实现了应用功能和底层功能的分离,框架/库的程序员可以专注于底层实现,借助反射实现通用功能,提供注解给应用程序员使用,应用程序员可以专注于应用功能,通过简单的声明式注解与框架/库进行协作。
我们再看 EasyPermissons 关于注解的处理就很简单了。
整理一下思路:
- 查找方法,找到具有 @AfterPermissonGranted 注解的方法
- 判断该方法的注解的元素是否与 RequestCode 相同
- 判断该方法的参数是否符合要求
- 暴力反射调用该方法
1 | private static void runAnnotatedMethods(@NonNull Object object, int requestCode) { |
结合实践:自定义 ButterKnife (伪)
结合上面所说的声明式编程的三步曲:
声明
1
2
3
4
5(RetentionPolicy.RUNTIME)
(ElementType.FIELD)
public BindView {
int value();
}解释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27private static void bindView(Activity context) {
Class<? extends Activity> aClass = context.getClass();
// 遍历所有的变量字段
Field[] declaredFields = aClass.getDeclaredFields();
for (Field field :
declaredFields) {
// 如果不可访问,设置为可访问
if (!field.isAccessible()) {
field.setAccessible(true);
}
// 如果字段有 BindView 注解,则读取其元素值进行操作
BindView bindView = field.getAnnotation(BindView.class);
if (bindView != null) {
int idRes = bindView.value();
View viewById = context.findViewById(idRes);
try {
field.set(context, viewById);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}使用
1
2
3
4
5
6
7
8
9
10
11
12(R.id.tv_content)
TextView mTextView;
(R.id.btn_change_content)
Button mButton;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_butter_knife);
ButterKnife.bind(this);
}
当然,以上只是简单的示例,真正的 ButterKnife 不是这样子实现的,它是使用了注解处理器(APT:相关类javax.annotation.processing
),在编译时对注解进行处理生成辅助代码及其他文件,从而避免降低运行时性能。编译时生成代码,可以在项目的build/generated/source/apt/目录下找到生成的代码,也可以对这些代码进行调试。现在主流的框架很多都使用这种方式来生成代码,比如 Dagger2、Glide、GreenDao、Room等。
其他
Android 提供了额外的注解,合理利用这些注解可以提高代码可读性和可维护性,Coding 时就可以避免错误,比如常用的 @Nullable、@Nonnull等,要查看更多相关注解及其使用请点击以下链接:
探究 Android 中的注解 : 介绍 Android 中的注解,以及 ButterKnife 和 Otto 这些基于注解的库的一些工作原理
Android 中注解的使用 : Android Support Library 提供的元注解介绍及使用,参考了上一文章。
Java 反射以及在 Android 中的特殊应用 : 文章介绍了反射在 Android 框架层的应用:作为四大组件的 Activity 其实也是一个普通对象,也是由反射创建。
本文的大部分内容摘自老马的《Java 编程的逻辑》进行整理,这本书对 Java 知识点的讲解清晰透彻,对入门的人深入理解 Java 很有帮助,推荐购买阅读。