对于 Android 注解, 或多或少都有一点接触, 但相信大多数人都是在使用其它依赖库的时候接触的. 因为有些库如果你想使用它就必须使用它所提供的注解. 例如: ButterKnife,Dagger2,Room 等等.
至于为何使用注解? 使用过的应该都知道, 最明显的就是方便, 简洁. 通过使用注解可以在项目编译阶段, 帮助我们自动生成一些重复的代码, 减轻我们的负担. 典型的 ButterKnife 本质就是使用 Android 注解, 通过注解来减少我们对 view.findViewById 的编写, 提高我们的开发效率. 上一个系列 (AAC) 的 Room 也是一样, 我们可以简单的回顾一下:
- @Entity(tableName = "contacts")
- data class ContactsModel(
- @PrimaryKey
- @ColumnInfo(name = "contacts_id")
- val id: Int,
- @ColumnInfo(name = "name")
- val name: String,
- @ColumnInfo(name = "phone")
- val phone: String
- )
通过使用注解来定义一个实体表, 也就 10 行左右的代码. 如果要我们全部自己写那绝对要两三百行代码了, 而且其中还可能出错, 又要改 bug 等等. 效率就严重降低. 对于依赖库如果都这么麻烦也就不会有人用了.
那么如何判断一个依赖库是否需要使用注解呢? 其实很简单, 只要记住以下两点即可:
需要生成的代码不能与项目逻辑有关
Android 注解只能生成代码, 并不能修改代码
这里透露一下, Android 注解的本质是使用 Java 的反射机制, 后续会详细说明
项目架构
相信 ButterKnife 应该有接触过吧, 没有的也没关系, 现在正是时候. 下面我们会自己实现 BindView 与 OnClick 注解, 实现 ButterKnife 中的对应注解功能. 那么我先来看下整体的项目架构
通过项目图, 我们可以清晰的看到, 主要分为三个部分
butterknife-annotations: 注解库, 包含 BindView 与 OnClick 等自定义的注解
butterknife-bind: 绑定库, 自定义的注解与声明的类绑定
butterknife-compiler: 解析编译生成库, 解析声明类中的注解, 在编译时自动生成相应的代码.
为了帮助大家能够更轻松的理解 Android 注解, 今天主要分析的就是 butterknife-annotations 这个注解库. 带大家一起来声明注解变量.
BindView
为了要实现开源库 butterknife 类似的绑定 id 效果, 这里我们先定义一个 BindView 注解, 具体如下:
- @Retention(RetentionPolicy.SOURCE)
- @Target(ElementType.FIELD)
- public @interface BindView {
- @IdRes int[] value();
- }
嗯, 还是很简单的对吧. 也就 5 行代码解决 BindView 注解的定义.
那么再来详细剖析这 5 行代码.
Retention
首先是第一行代码的 Retention, 看它的使用方式就能知道, 它也是一个声明了的注解.
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.ANNOTATION_TYPE)
- public @interface Retention {
- /**
- * Returns the retention policy.
- * @return the retention policy
- */
- RetentionPolicy value();
- }
通过源码我们可以看出该注解只接收一个参数, 该参数为 RetentionPolicy 类型. 那么我们在进一步深入 RetentionPolicy:
- public enum RetentionPolicy {
- /**
- * Annotations are to be discarded by the compiler.
- */
- SOURCE,
- /**
- * Annotations are to be recorded in the class file by the compiler
- * but need not be retained by the VM at run time. This is the default
- * behavior.
- */
- CLASS,
- /**
- * Annotations are to be recorded in the class file by the compiler and
- * retained by the VM at run time, so they may be read reflectively.
- *
- * @see java.lang.reflect.AnnotatedElement
- */
- RUNTIME
- }
在这里我们发现它其实是一个枚举, 在枚举中支持三个常量, 分别为 SOURCE,CLASS 与 RUNTIME. 它们的区别主要是作用的周期范围, 下面我再对这三个的作用进行翻译一遍:
SOURCE: 使用该标明的注解将在编译阶段就被抛弃掉.
CLASS: 使用该标明的注解将在编译阶段记录到生成的 class 文件中, 但在运行阶段时又会被 VM 抛弃. 默认是该模式.
RUNTIME: 使用该标明的注解将在编译阶段被保存在生成的 class 文件中, 同时在运行阶段时会保存到 VM 中. 所以它该注解将一直存在, 自然能够通过 java 的反射机制进行读取.
所以它们的存在的生命时长为 SOURCE < CLASS < RUNTIME. 知道了它的作用范围之后, 我们在自定义注解时就要尽量较小注解的作用范围, 提高项目的编译与运行速度.
因为我们的 BindView 注解只是为了进行 Viwe 的绑定, 所以在编译之后就无需存在, 所以这里就使用了 CLASS 来进行标明.
Target
下面我们在来看第二行代码, 这里使用到了另一个注解 Target, 我们还是来看下它的源码:
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.ANNOTATION_TYPE)
- public @interface Target {
- /**
- * Returns an array of the kinds of elements an annotation type
- * can be applied to.
- * @return an array of the kinds of elements an annotation type
- * can be applied to
- */
- ElementType[] value();
- }
可以看到注解的源码都非常简单, 这里接收了一个 ElementType 数组参数, ElementType 不难猜出它的类型也是一个枚举:
- public enum ElementType {
- /** Class, interface (including annotation type), or enum declaration */
- TYPE,
- /** Field declaration (includes enum constants) */
- FIELD,
- /** Method declaration */
- METHOD,
- /** Formal parameter declaration */
- PARAMETER,
- /** Constructor declaration */
- CONSTRUCTOR,
- /** Local variable declaration */
- LOCAL_VARIABLE,
- /** Annotation type declaration */
- ANNOTATION_TYPE,
- /** Package declaration */
- PACKAGE,
- /**
- * Type parameter declaration
- *
- * @since 1.8
- */
- TYPE_PARAMETER,
- /**
- * Use of a type
- *
- * @since 1.8
- */
- TYPE_USE
- }
ElementType 中虽然有 10 常量, 但我们实际真正常用的也就是前面 8 种. 它们代表自定义的注解能够作用的对象. 分别为:
TYPE: 作用于类, 接口或者枚举
FIELD: 作用于类中声明的字段或者枚举中的常量
METHOD: 作用于方法的声明语句中
PARAMETER: 作用于参数声明语句中
CONSTRUCTOR: 作用于构造函数的声明语句中
LOCAL_VARIABLE: 作用于局部变量的声明语句中
ANNOTATION_TYPE: 作用于注解的声明语句中
PACKAGE: 作用于包的声明语句中
TYPE_PARAMETER:java 1.8 之后, 作用于类型声明的语句中
TYPE_USE:java 1.8 之后, 作用于使用类型的任意语句中
结合我们的 BindView 的作用是对 View 进行 id 绑定, 自然是作用与声明的字段上. 所以在 BindView 中使用了 FIELD.
再来看第四行代码
@IdRes int[] value()
有了上面的注解接触, 不难理解这是标明 BindView 将接收一个 int 类型的数组参数. 对于开源库 butterknife 中的 BindView 是接收需要绑定的 View 的 id, 这里我们做一个改版, 再接收一个 String 的 id, 用来为绑定的 View 设置默认值. 这样我们自定义了的 BindView 注释就完成了.
OnClick
下面我们再自定义一个 OnClick 点击的注解, 经过上面的分析, 可以在脑海中想想 Retention 与 Target 分别什么值?
想好了之后我们在来过一遍
- @Retention(RetentionPolicy.SOURCE)
- @Target(ElementType.METHOD)
- public @interface OnClick {
- @IdRes int value();
- }
Retention 的作用范围与 BindView 一样首页 SOURCE, 在编译之后就无需存在; Target 的作用对象与 BindView 不同, 既然是点击事件的点击操作, 自然是作用在操作逻辑的方法上, 所以这里使用 METHOD.
keep
文章开头有提及到本质是通过注解来自动生成代码, 为我们创建所需的类, 那么在实际开发中一旦我们的项目混淆了, 这将会导致自动创建的类失效, 从而导致我们自定义的注解失效. 所以为了防止其失效, 我们在这里再定义一个注解 keep:
- @Retention(RetentionPolicy.CLASS)
- @Target(ElementType.TYPE)
- public @interface Keep {
- }
Retention 的作用范围是在 class 文件中还要能够被其它 class 调用, 所以这里使用 CLASS;Target 作用对象是自动生成的类, 所以使用 TYPE. 至于参数则不必要, 它只是为了标明类, 防止其被混淆.
总结
butterknife-annotations 库中的自定义注解就完成了. 通过上面的分析, 我们注意点主要归结于以下三点:
Retention: 明确注解作用的生命时长, 尽早的消除
Target: 明确注解作用的对象
Keep: 防止后续自动生成的类被混淆
注解变量的定义就到这结束了, 同时文章中的代码都可以在 Github https://github.com/idisfkj/android-api-analysis 中获取到. 使用时请将分支切换到 feat_annotation_processing
来源: https://segmentfault.com/a/1190000015468666