上一篇写动态代理的时候,拿Retrofit来举例子,提到了Retrofit在使用的时候通过注解来进行参数的配置,通过@Get()/@POST()/@Path()/@Query()...等注解进行参数声明,就可以调用OkHttp的时候发送正确的报文
在平时开发使用第三库的时候,使用的时候经常会遇到通过注解去声明参数的情况
- 使用butterknife通过@BindView()注解就可以将控件的id和声明的控件成员变量进行绑定,省略了findViewById的的手动绑定过程
- 使用dagger注入成员的变量的时候,通过@Inject注解就可以去获取@Module中通过@Provides声明提供的实例化对象
- 使用webview和js代码交互的时候,通过@JavascriptInterface注解可以声明web页面可以使用的方法
还有很多在使用第三方库的时候使用注解的情况,除了以上在项目开发中经常用到的,我相信跟我一样在网上搜索过内存优化的朋友,会看到过关于在使用枚举类约束变量参数取值的情况使用@IntDef注解去替代,因为枚举值都会封箱成一个对象,对象就会有额外的内存开销
//使用枚举,枚举值都是一个对象
public enum Type {
TYPE_1, TYPE_2
}
//枚举的使用
public class Test {
private static Type mType;
//参数类型就声明的枚举类型
public static void setType(Type type) {
mType = type;
}
public void test() {
//方法调用时参数只能是枚举中声明的类型
setType(Type. TYPE_1);
}
}
如果我们使用@IntDef,就需要自己声明一个注解,并且用@IntDef对其注解
public class Test {
private static int mCurrentType;
public static final int TYPE_1 = 1;
public static final int TYPE_2 = 1<<1;
//通过参数的注解,从而来限定传入参数的值进行校验
public static void setType(@TypeAnnotation int type) {
mCurrentType = type;
}
public void test() {
setType(TYPE_1);//可选参数就是在注解中传入的值
setType(1); //直接传参编译器会有错误提示
}
}
//声明的参数注解
@IntDef(value = {Test.TYPE_1, Test.TYPE_2}) //通过IntDef元注解,传入对应的int值
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.SOURCE)
public @interface TypeAnnotation { //注解
}
相比较两个静态int常量值的内存,使用枚举去进行参数限定会占用更多的内存
什么是注解
举例了很多应用场景,第三方库的封装也经常使用注解,注解应该是一个非常有用的东西,但是实际上它在项目运行中又没有什么实际的作用,不参于运算,不改变执行逻辑,只是在对应的代码出打上标记,开发者在使用场景中,通过拿到注解和注解的值,去添加相应的处理逻辑
注解的使用
注解在使用上也很简单,类似于写一个类通过class关键字进行声明,注解通过@interface进行声明
public @interface BitBaba {}
Target元注解
然后还需要给注解需要标记的位置通过@Target进行限定,能作用在注解上的注解叫做元注解,jdk中也默认提供了一些元注解,@Target就是其中一个
@Target({ElementType.PARAMETER})//可以传入多个值,作用域确定后该注解就限定在对应类型的位置使用
public @interface BitBaba {}
//Target可以的取值类型
public enum ElementType {
TYPE,//类、接口和枚举
FIELD,//字段
METHOD,//方法
PARAMETER,//参数
CONSTRUCTOR,//构造函数
LOCAL_VARIABLE,//本地变量
ANNOTATION_TYPE,//注解类型
PACKAGE,//包
TYPE_PARAMETER,//类型参数声明,JavaSE8引进,可以应用于类的泛型声明之处
TYPE_USE//JavaSE8引进,此类型包括类型声明和类型参数声明,是为了方便设计者进行类型检查
}
Retention元注解
声明了作用域,规定了注解的标记位置,还需要一个通过另一个元注解@Retention确定注解的保留阶段
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.SOURCE)//声明注解的保留阶段
public @interface BitBaba {}
public enum RetentionPolicy {
SOURCE,//保留在源码阶段,被编译期忽略
CLASS,//编译时由编译器保留,但是jvm虚拟机会忽略
RUNTIME//在jvm保留,在运行时环境中可以使用它
}
@Retention中的三个值是包含关系,SOURCE<CLASS<RUNTIME,选择了RUNTIME阶段,那么在源码阶段和编译时都会存在
注解参数
既然是一个标记的作用,那么为了使用场景,当然是有传值的需求,就比如butterknife中需要通过注解传入控件的id
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)//声明注解的保留阶段
public @interface BitBaba {
String value(); //没有默认值
int age() default 30; //给默认值
}
//有默认值的参数在使用注解是可以默认不传值
@BitBaba("实际") //如果只是value需要传值的情况下,可以隐式的传参,不用写对应的参数名:
@BitBaba(value ="梦想",age=18)
Object o;
注解应用
上面的部分我们已经声明一个我们自己的注解类,接下来就总结一下这个注解到底可以怎么用
通过@Retention元注解,可以制定注解的保留时期是不一样的,那么自然相应的应用场景也不一样,分开来说明
SOURCE源码级别的注解
- IDE语法检查
源码级别的注解,由于在javac编译后的.class文件中不会保留,所以这个注解的应用场景就在javac编译完成之前,比如上面做枚举优化是提到的IntDef注解,就是一个源码级别的注解
@Retention(SOURCE)//源码级别
@Target({ANNOTATION_TYPE})
public @interface IntDef {
int[] value() default {};
boolean flag() default false;
boolean open() default false;
}
这类注解可以用来给IDE进行语法检验,限定参数的传值范围就是其中的一个应用场景
- APT
除了语法检查,源码级别的注解的一个常见的应用场景就是做APT(Anotation Processor Tools),也就是注解处理器,注解处理器是javac的一个工具,在执行javac编译的时候,会扫描注册的注解处理器,然后就可以在标记的位置做额外的逻辑,常见的可以自己手写类文件,比如butterknife就是通过APT技术,把注解的值获取手,统一封装了findviewbyid的逻辑
第三方库在依赖的时候,需要使用annotationProcessor去依赖的,就是使用了APT技术,这个的具体应用后面找文章再写了
CLASS级别的注解
这个级别的注解,会保留在编译后的class文件中,但是虚拟机运行的时候会忽略掉注解编译后的那部分class文件,就是无法在运行时通过反射去获取注解,这个级别的注解的应用场景就是针对字节码进行操作,应用在一些AOP的场景中,比如实现统一的登录逻辑判断,或者权限申请这些
对于字节码的处理,纯手写还是很困难的,借助一些第三方框架,比如AspectJ,就可以对字节码进行更符合语言特性的插入修改,字节码的操作也是群里大佬经常讨论的热修复技术,里面有提到字节码插桩,这部分我没在项目中应用过,后续等我在网上学习一下后写一篇总结
RUNTIME级别的注解
这种情况下注解会保留到运行期,那么我们就可以在运行期通过反射可以获取到注解的信息
在比较早的butterknife版本中,就是通过运行时反射,比如在Activity的oncreate()方法中,调用butterknife的注入方法,这个方法就是利用反射获取声明的控件注解,获取到对应的控件id,然后调用findviewbyid进行实例化赋值
找不到源码了,自己弄一个简单的例子
//声明一个运行期的注解,作用在成员变量上
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
public class ButterKnife {
public static void bindViews(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
// 获取类的所有域变量
Field[] fields = clazz.getFields();
for (Field field : fields) {
// 获取BindView注解
BindView bindAnno = field.getAnnotation(BindView.class);
if (bindAnno != null) {
// 存在有被@BindView注解标记的属性
int id = bindAnno.value();
try {
// 获取findViewById的Method实例
Method bindMethod = clazz.getMethod("findViewById", int.class);
// 调用bindMethod来获得View
Object view = bindMethod.invoke(activity, id);
// 私有变量给与赋值权限
field.setAccessible(true);
// 将结果赋值给field
field.set(activity, view);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
//最后再对应需要绑定的Activity页面的onCreate后面,调用对用的
ButterKnife.bindViews(this); //在界面需要绑定的地方调用这个方法就行
类似的运行期注解的使用方法如上,主要还是需要结合反射的技术去获取注解和注解的值,使用反射当然比直接在代码中进行绑定性能是有损耗的,但是这样可以将将重复的绑定逻辑进行优化
现在版本的ButterKnife已经使用了APT技术,通过生成代码的方式,优化了反射带来的性能问题
总结
除了使用第三方库,在实际的项目开发中确实不需要使用注解也能写好一个程序,但是不用不代表不需要了解,不了解的话就没法在群里吹逼,大佬们的发言也感觉自己更加的菜狗,要是面试被问到也会一脸懵逼,而且看第三方库也手脚搞不赢
菜如我没去网上学习反射这部分连gradle依赖为什么需要使用“annotationProcessor”而不是用“implementation”都不清楚
写文章也是为了自己学习,还是希望有路过的大佬能指点一下