Java注解是JDK5.0引入的。它是元数据的一种形式,提供有关程序而不属于程序本身的数据。注解默认实现Annotation接口(这一点类似于java类默认都继承自Object)。声明一个注解用@Interface关键字。
// Java lang包下的 Annotation.java
package java.lang.annotation;
public interface Annotation {
boolean equals(Object var1);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
}
// 定义一个自己的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface AnnoTest {
String strValue();
int intValue() default 1;
}
// 使用自定义注解
@AnnoTest(strValue = "test", intValue = 2)
public class MyAnnoTest {
}
元注解
元注解,是注解的注解。在自定义注解时,用元注解来对注解进行修饰。需要用到的元注解有两个@Target和@Retention。另外还有@Documented与@Inherited,@Documented前者被用于被javadoc工具提取成文档;@Inherited表示允许子类继承父类中定义的注解。
@Target用于修饰注解作用的对象。
ElementType.ANNOTATION_TYPE //应用于注解类型
ElementType.CONSTRUCTOR //应用于构造函数
ElementType.FIELD // 应用于字段或属性
ElementType.LOCAL_VARIABLE // 应用于局部变量
ElementType.METHOD // 应用于方法级注解
ElementType.PACKAGE // 应用于包声明
ElementType.PARAMETER // 应用于方法的参数
ElementType.TYPE //可以应用于类的任何元素
@Retention用于表明注解的保留级别
RetentionPolicy.SOURCE // 保留到源级别,在编译时会被忽略
RetentionPolicy.CLASS // 保留到编译级别,在编译时由编译器保留,运行时会忽略
RetentionPolicy.RUNTIME // 保留到运行级别,在JVM运行时可以使用
注解保留的级别后者会包含前者,既CLASS级包含了SOURCE级,RUNTIME级包含CLASS级。SOURCE < CLASS < RUNTIME。
注解使用场景
Source保留级别
RetentionPolicy.SOURCE,是源码级别的注解,通常用于提供IDE语法检查、APT技术等场景。
IDE语法检查:
例如,自定义一个注解,对方法的入参进行限制,替代枚举的作用。
// 定义一个AWeekEnd注解,声明作用于函数的入参,保留到源码级别
@IntDef(value = {WeekEnd.Saturday, WeekEnd.Sunday})
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.SOURCE)
public @interface AWeekEnd {
int Saturday = 1;
int Sunday = 2;
}
// 方法的参数使用@AWeekEnd注解修饰
public void playOnWeekEnd(@AWeekEnd int weekEnd) {
// ...
}
public void test() {
// 调用该方法的参数需要传AWeekEnd.Saturday或AWeekEnd.Sunday
// 否则语法检查会报错
playOnWeekEnd(AWeekEnd.Saturday);
// playOnWeekEnd(AWeekEnd.Sunday);
}
调用上面的playOnWeekEnd方法,如果传入的不是AWeekEnd.Saturday或AWeekEnd.Sunday的话,IDE语法检查会提示错误:
这样替代枚举的意义是会比枚举更加节省空间。一个int类型数占4个字节。而枚举类型每一个都是对象,占据空间要大的多。前面在博客“Jvm对象初始化过程”中提到对象的组成部分,包括对象头、实例数据和填充数据。对象头占12字节,另外需要进行8字节对齐(填充数据),一个对象最少需要占16位。
另外,这里要用到androidx.annotation提供的@IntDef注解。这是一个元注解,定义如下。
// androidx.annotation IntDef注解
@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
int[] value() default {};
boolean flag() default false;
boolean open() default false;
}
另外,AS可以修改语法检查的级别:
APT技术
APT全称为:"Anotation Processor Tools",即注解处理器。APT是javac自带的一个工具,用于在将Java源文件编译成Class文件时,扫描源文件中的注解信息,将其传递给注解处理器进行处理。我们可以自定义注解处理器,针对特定的注解进行处理。当前许多著名的开源框架都采用了APT的技术。如Glide、EventBus3.0、ARouter等。APT技术的实现,在后面博客再专门讨论。
Class保留级别
此类注解会保留到经过javac编译的到的Class文件中。应用场景通常为需要进行字节码插桩的场景。如,Roubust热修复技术,用与解决类被带上CLASS_ISPREVERIFIED标签的问题。字节码插桩,既是修改字节码(Class)文件,进行代码逻辑的修改。
Android中插桩的点,一般选择在dex工具将Class文件转换为dex文件时。gladle编译过程中"transformClassesWithDexBuilderForDebug" task既是将class文件转换为dex文件,输入是.class文件,输出是.dex文件。在gradle中,增加自定义的编译插件,拦截该task,对class文件进行修改(可以用ClassVisitor工具)。关于字节码插桩、gradle插件在后面博客再单独讨论。
举一个例子,简单说明结合注解进行字节码插桩后的效果。自定义Login注解,标识调用该方法需要先进行登录。在编译后的字节码文件,插入是否登录和跳转到登录页的功能:
// 定义Login注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Login {
}
// Java源文件
@Login
public void jump(){
startActivity(new Intent(this,AActivity.class));
}
// 编译后字节码文件
@Login
public void jump() {
this.startActivity(new Intent(this, AActivity.class));
}
// 插桩修改后字节码文件
@Login
public void jump() {
if (this.isLogin) {
this.startActivity(new Intent(this, LoginActivity.class));
} else {
this.startActivity(new Intent(this, AActivity.class));
}
}
Runtime保留级别
此类注解,能够保留到运行期。通常是结合反射技术获取注解的信息,并根据信息做对应的处理。
举一个基于注解,在运行时进行控件自动注入的例子:
// 定义一个注解,作用在成员变量上,需要一个资源id号的参数
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectView {
@IdRes int value();
}
// 在MainActivity.java中使用注解
public class MainActivity extends Activity {
// 用注解修饰需要注入的控件,将控件的id作为注解的参数
@InjectView(R.id.tv)
private TextView tv;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 调用方法进行动态注入
InjectUtils.injectView(this);
}
...
}
// InjectUtils.java中进行动态注入
public class InjectUtils {
public static void injectView(Activity activity) {
Class<? extends Activity> cls = activity.getClass();
// 获得此类所有的成员
Field[] declaredFields = cls.getDeclaredFields();
// 对所有成员进行遍历,找到被InjectView修饰的成员
for (Field filed : declaredFields) {
// 判断属性是否被InjectView注解声明
if (filed.isAnnotationPresent(InjectView.class)) {
InjectView injectView = filed.getAnnotation(InjectView.class);
// 获得了注解中传递的参数,控件的id
int id = injectView.value();
View view = activity.findViewById(id);
// 设置访问权限,允许操作private的属性
filed.setAccessible(true);
try {
// 通过反射对属性进行赋值,完成对控件注入
filed.set(activity, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}