定义(What)
注解,是源代码的元数据。
两个本质:
- 它就是一个附属品,依赖于其他元素存在
- 本身没有任何作用,在恰当的时候由外部程序解析产生作用。
作用(Why)
- 简化配置
- 增加代码可读性
- 提升系统可维护性
类型(How)
按定义分类:
分为内置注解和自定义注解。
内置注解如下:
- @Override
- @Deprecated
- @SuppressWarnings
自定义注解?android.support.annotation包下全是自定义注解,不知道大家注意过没。贴个图~
按生命周期分类(重点)
- SOURCE:表示在编译时这个注解会被移除,不会包含在编译后产生的 class 文件中
- CLASS:边上这个注解会被包含在 class 文件中,但运行时会被移除
- RUNTIME:表示这个注解会被保留到运行时,在运行时可以 JVM 访问到,我们可以在运行时通过反射解析到这个注解。
可能有同学会问:不管我是用于编译时代码生成还是运行时反射处理,我直接对所有注解申明RetentionPolicy.RUNTIME不就好了吗?或者即使我想在编译时代码生成我也用RetentionPolicy.SOURCE,也是可以的吧?
没错,RetentionPolicy.RUNTIME是优先级最大的修饰,但为什么不建议呢?这个的原因同修饰类成员时用的private还是public得道理一样。
元注解(重点)
磨刀不误砍柴工,先弄清楚“元注解”,然后我们再来学习自定义注解。
Retention定义注解生命周期,可选为:source、class、runtime(生命周期介绍如上)
Documented 文档化注解,会被 Javadoc 工具文档化
Inherited注解自动集成的,想让一个类和他的子类都包含某个注解,就可以使用他来修饰这个注解。
Target说明了被修饰的注解的应用范围,包括:
- TYPE:表示可以用来修饰类、接口、注解类型或枚举类型
- PACKAGE:可以用来修饰包
- PARAMETER:可以用来修饰参数
- ANNOTATION_TYPE:可以用来修饰注解类型
- METHOD:可以用来修饰方法
- FIELD:可以用来修饰属性(包含枚举常量)
- CONSTRUCTOR:可以用来修饰构造器
- LOCAL_VARLABLE:可以用来修饰局部变量
敲黑板,Retention 和 Target 是重点,METHOD 是 Target 的重点。
自定义注解
首先我们来创建一个注解类,其实就是一个接口前面加了一个@符号。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface CacheResult {
String key();
String cacheName();
String backupKey() default "";//备份缓存
boolean needBloomFilter() default false;//解决缓存击穿
boolean needLock() default false;//是否开启读取缓存线程锁
}
然后我们用Retention给注解设置生命周期为 Runtime,用 Target 为注解设置这个注解可以用来修饰类和方法。然后我们给注解添加了五条属性,其中前两条没有默认值(使用的时候需要手动设置,格式如下 demo)、后三条有默认值(可选设置值)。
以上注解是从 spring 框架中 copy 的,那5条属性没看懂没事哈,都是自定义的,可以随便删,我们主要是学习注解怎么用。
public class TestAnnotation {
public static void main(String[] args) {
Class<TestBean> testBeanClass = TestBean.class;
Class<CacheResult> cacheResultClass = CacheResult.class;
if (testBeanClass.isAnnotationPresent(cacheResultClass)) {
CacheResult annotation = testBeanClass.getAnnotation(cacheResultClass);
System.out.println(annotation.key());
System.out.println(annotation.cacheName());
Method[] methods = testBeanClass.getMethods();
for (Method m : methods) {
if (m.isAnnotationPresent(cacheResultClass)) {
CacheResult a = m.getAnnotation(cacheResultClass);
System.out.println(a.key());
System.out.println(a.cacheName());
}
}
}
}
@CacheResult(key = "class", cacheName = "className")
public class TestBean {
@CacheResult(key = "method", cacheName = "methodName")
public void test() {
}
}
}
打印结果
class
className
method
methodName
Process finished with exit code 0
通过以上 demo,我们要记住注解的两个本质:
- 它就是一个附属品,依赖于其他元素存在
- 本身没有任何作用,在恰当的时候由外部程序解析产生作用。
结合 Retrofit
我们都记得 Retrofit 的Api 是基于注解的,我们来看看 Retrofit 是怎么读取注解的
上图这个过程是Retrofit 在读取 AppApiService接口的这个方法。
/**
* 获取首页图片
*
* @param size 获取图片张数
*/
@GET("http://lab.zuimeia.com/wallpaper/category/1/")
Observable<HttpResultV1<ImageData>> getImage(@Query("page_size") int size);
我们来简单看一下ServiceMethod.Builder 类的构造方法做了些什么事
public Builder(Retrofit retrofit, Method method) {
this.retrofit = retrofit;
this.method = method;
this.methodAnnotations = method.getAnnotations();//获取方法上的注解
this.parameterTypes = method.getGenericParameterTypes();//获取方法参数列表
this.parameterAnnotationsArray = method.getParameterAnnotations();//获取方法参数的二维数组,为什么是二维数组呢,因为一个参数可以有多个注解呀。
}
然后就是 build 方法解析注解了。
public ServiceMethod build() {
for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}
int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];
for (int p = 0; p < parameterCount; p++) {
Type parameterType = parameterTypes[p];
if (Utils.hasUnresolvableType(parameterType)) {
throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",
parameterType);
}
Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
if (parameterAnnotations == null) {
throw parameterError(p, "No Retrofit annotation found.");
}
parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
}
}
以上就是 build 方法中解析注解的部分代码,细节就不去仔细看了。具体 Retrofit 是怎么解析这些参数以及解析的这些参数是干嘛的不是我们这里关心的内容。
自定义注解(重点)
上面我们学了
- 使用jdk 反射获取注解信息
- 在 Retrofit 中是怎么获取注解信息的
这里我抛出两个问题:
- 注解好像仅仅是用于解析自定义注解信息,好像并没有什么卵用啊
- 你用的最多的自定义标签有哪些?
在我们常用的 ButterKnife 中,就用了一个BindView 注解,就可以成功给 View 初始化
@BindView(R.id.tv_welcome)
TextView mTvWelcome;
这里我们来研究一下究竟是怎么办到的。
首先我们来看 BindView 的代码
@Retention(CLASS) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
源码中这些信息应该都看得懂吧,前面都讲了的,我们来简单看一下@IdRes 的返回值限定
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface IdRes {
}
这里只是一种类型限定,限定了返回类型只能说 Res 的资源。
而BindView的value 方法是没有 default 值的,而 value 的值又限定死了只能是 int 型的 Res 资源。所以@BindView(R.id.tv_welcome)括号里面需要给注解赋值。
然后问题来了,前文我们强调过注解的本质,我们再来回顾一下:
- 它就是一个附属品,依赖于其他元素存在
- 本身没有任何作用,在恰当的时候由外部程序解析产生作用。
那么被 BindView标注的 TextView mTvWelcome 是怎么被赋值的呢,我们来看看ButterKnife.bind() 方法,这个方法一般是在 onCreate 方法里面调用,传参一般是用 this。调用了这个方法,然后mTvWelcome字段有了BindView 注解就被赋初始值了。那么我们来看看 bind 方法里面的执行吧。
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
这个代码很简单,直接跳过了。
这个代码也简单,findBindingConstructorForClass方法找到了一个叫LaunchActivity_ViewBinding 的类,然后再调用构造方法。然后,然后就结束了?这个 LaunchActivity_ViewBinding 类特么是个什么鬼。。。
然后我在 build 文件里面找到了这个类~
在这里,我们找到了 findViewById 的方法给mTvWelcome做了初始化操作。
然后还剩下一个问题,这个 LaunchActivity_ViewBinding 到底是从哪里来的。
这个问题好像有点超纲了,我简单介绍一下吧,LaunchActivity_ViewBinding 是由APT 生成的,不知道在引入 ButterKnife 的时候大家是否还记得在 gradle 里面加了一行这样的代码apt com.jakewharton:butterknife-compiler:8.4.0
.没错,这些build 文件里面的代码都是 apt 生成的。
APT
APT英文全称:Android annotation process tool是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。
Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成源文件和原来的源文件,将它们一起生成class文件。简言之:APT可以把注解,在编译时生成代码。
ButterKnife 中 apt的大致工作流程就是这样:代码编写完,在编译的时候扫描java 文件,对包含某些特定注解的 java 文件进行扫描获取字段及其注解的值,然后使用诸如“javapoet”之类的代码生成工具生成build 目录中的***Activity_ViewBinding 文件。
自定义注解实现编译时检查代码
如图,使用了 NotNull 注解标识的参数,如果传 null 会报黄色警告。
纳尼,注解的本质我都能背得下来了,我背给你看~
它就是一个附属品,依赖于其他元素存在
本身没有任何作用,在恰当的时候由外部程序解析产生作用。
说好的没有任何作用呢~ 看了@NotNull 的源码,好像源码用的几个关键字也没有让代码报黄色警告的意思啊。
于是,不服气的我把 @NotNull 代码 copy 出来,写了一个新的自定义注解 @No。
What the Fuck!!!为什么我自己写的注解没有黄色警告。心凉了三分钟~
好吧,通过上面的测试不得不承认,这个黄色警告跟代码没关系,大概是编译器的功能吧。于是,经过一番 Baidu 和咨询好友。找到了这个功能~
然后你也可以修改警告级别。
同样用于代码提示的注解还有 @Override(标记重写父类的方法)、@Deprecated(过期方法)、@SuppressWarnings(忽略警告)。
注解就到这里吧,高阶玩法可以去研究一下 Retrofit 或者 ButterKnife 的 apt。java Srping 框架的注解玩也玩很溜,对后台感兴趣的可以去学习。