在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。这就是注解,比如@Override,@Deprecated。

引言

1//View.java#findViewById
2@Nullable
3public final <T extends View> T findViewById(@IdRes int id) {
4    if (id == NO_ID) {
5        return null;
6    }
7    return findViewTraversal(id);
8}

在安卓中比较常见的一个方法,findViewById。我们在写代码的过程中如果直接这样写,会提示错误。虽然编译不会出错,但能够在编码阶段做一个语法检查,提示我们需要用R.id.xx的形式来传入变量值。

1findViewById(1001);
findViewById findViewById

实际上里面包含了注解的使用,其中这里面的@Nullable和@IDRes就是注解,但这两个注解又不一样。一个注解放在了方法前,显然是修饰整个方法;一个注解放在了方法形参变量的前面,显然这个是修饰形变量。

1//这个注解提示修饰的值可以为null
2@Retention(SOURCE)
3@Target({METHOD, PARAMETER, FIELD})
4public @interface Nullable {
5}

@Nullable修饰整个方法,含义是该方法返回的值可以为null

1//这个注解提示修饰的值可以为null
2@Documented
3@Retention(CLASS)
4@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
5public @interface IdRes {
6}

@IdRes修饰形参变量,含义是修饰的实参变量必须是R.id.xx的形式。我们在编码过程中恰好是以这样的方法传入参数的,这就可以说明一件事,一切都有迹可循。

内心OS:其实到这边还是有点懵的,这个类怎么这么奇怪,又是interface,会是@符号,究竟是什么来头?

类前面加了好几个@形式的语句,也不知道是啥含义?

带着这两个问题往下看。

注解的含义

那注解是什么?

注解本身没有意义,单独的注解就是一种注释,他需要结合反射、插桩等技术才有意义。

另外Java注解又称为Java标注,是元数据的一种形式,提供有关程序但不属于程序本身的数据。注解对他们注解的代码的操作没有直接影响。

回答上面的问题

其实这里面的@interface是专门用于指代该类为注解类,且可以用@类名的形式,表示当前注解。

而对于类前@形式的语句,实际上是修饰注解类的注解,听起来有点绕。说白了就是使得定义的注解起作用,必须依赖这个注解类的注解,而这个注解类的注解即为元注解。

元注解(meta-annotation)

元注解标准 注解详情 作用
@Target 限定注释类修饰对象
@Retention 注解保留的时间范围
@Documented / 为类生成帮助文档时是否要保留其注解信息,注解只是用来生成文档的,并不重要这里不具体展开
@Inherited / 子类将自动具有该注解

详情可见如下所示

注解详情 注解详情

其中,@Target详情中的ElementType,描述作用域

Element 名称
TYPE 类、接口、枚举
FIELD 成员变量(包括枚举变量)
METHOD 成员方法
PARAMETER 方法参数
CONSREUCOR 构造方法
LOCAL_VARIABLE 局部变量
ANNOTATTION_TYPE 注解类
PACKAGE
TYPE_PARAMETER 类型参数,jdk1.8新增
TYPE_USE 使用类型的任何方法,jdk1.8新增

其中,@Retention详情中的RetentionPolicy,描述作用期

RetentionPolicy 名称
SOURCE 源文件保留
CLASS 编译器保留,默认值
RUNTIME 运行期保留,可通过反射去获取注解信息

注解场景

SOURCE

RetentionPolicy.SOURCE 作用于源码级别的注释,这个场景主要用于APT和检查语法。

一般编译,java文件通过jdk工具javac会先编译成二进制的机器码.class,cafe babe开头表示这个是一个class文件。如果我们要看class文件,需要反编译javap才能看到方便阅读的内容。

当然Androidstudio中的Plugins插件中心的Market搜索ASM,即可安装字节码工具,可以直接看.Class,非常方便。

as中的插件 as中的插件

通过常见的Activity中oncreate方法前的override修饰,在类中使用 SOURCE 级别的注解,其编译之后的class中会被丢弃

 1//MainActivity.java
 2@Override
 3protected void onCreate(@Nullable Bundle savedInstanceState) {
 4    super.onCreate(savedInstanceState);
 5    setContentView(R.layout.activity_main);
 6    InjectUtils.injectView(this);
 7
 8    Intent intent = new Intent(this, SecondActivity.class)
 9        .putExtra("name", "23"); //传10个参数
10    startActivity(intent);
11}
12//MainActivity.class
13//与上面的区别就是没有override修饰,并且setContentView显示的为数字
14protected void onCreate(@Nullable Bundle savedInstanceState) {
15    super.onCreate(savedInstanceState);
16    this.setContentView(2131296284);
17    InjectUtils.injectView(this);
18    Intent intent = (new Intent(this, MainActivity.SecondActivity.class)).putExtra("name", "23");
19    this.startActivity(intent);
20}

APT(Annotation Processor Tools 注解处理器)

主要步骤如下:

1.源码级别的注解

 1public class MyProcessor extends AbstractProcessor {
 2
 3    @Override
 4    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
 5        Messager messager = processingEnv.getMessager();
 6        messager.printMessage(Diagnostic.Kind.NOTE, "================================");
 7
 8        return false;
 9    }
10}

这里不具体展开关于自定义的注解处理器。后续框架源码分析ButterKnife,会详细说明。

2.注册APT

定义一个resources,里面穿件一个文件夹MRTA-INF.services,里面有唯一的文件叫javax.annotation.processing.Processor

注册APT的文件 注册APT的文件

里面写注解处理程序的完整路径

3.采集到所有的注解信息 -> Element -> 注解处理程序

4.注解处理程序为MyProcessor

MyProcessor log MyProcessor log

IDE语法检查

语法检查是IDE实现的

举一个比较常见的例子,用注解替换掉原来的Enum类。

因为一个对象占用字节头部 12个字节+ 8字节对齐,非常消耗内存空间。

 1//自定义一个注解类,让注解标注的方法,属性和传入参数都限制只使用特定的输入,不然报错
 2//@MyAnnotation.java
 3//限定词IntDef,即虽然定义的是int类型,但是必须使用下面限定的写法,不然报错
 4//Target限制作用域,根据前文可知,这里限制了传参,属性和方法
 5//Retention定义了场景为源码阶段,编译生成的class类型不会出现这个注解
 6//在类里面定义的几个变量,默认为public static final修饰,可以省略不写
 7@IntDef(value = {MyAnnotation.custom_1,
 8        MyAnnotation.custom_2,
 9        MyAnnotation.custom_3,
10        MyAnnotation.custom_4,
11        MyAnnotation.custom_5,
12        MyAnnotation.custom_6})
13@Target({ElementType.PARAMETER,ElementType.FIELD,ElementType.METHOD})
14@Retention(RetentionPolicy.SOURCE)
15public @interface MyAnnotation {
16    int custom_1 = 1;
17    int custom_2 = 2;
18    int custom_3 = 3;
19    int custom_4 = 4;
20    int custom_5 = 5;
21    int custom_6 = 6;
22}

已经定义完了自定义注解@MyAnnotation,下面就开始定义例子,在例子里面具体看这个@MyAnnotation注解的作用

 1//在activity里面简单的写入属性i,传参state,方法getState
 2public class MainActivity extends AppCompatActivity {
 3    @MyAnnotation
 4    private int i = 1;//错误,应该修改成private int i = MyAnnotation.custom_1;
 5
 6    public void setState(@MyAnnotation int state){
 7        this.i = state;
 8    }
 9    
10    @MyAnnotation
11    public int getState(){
12        return 1;//错误应该改成 return MyAnnotation.custom_1;
13    }
14
15    @Override
16    protected void onCreate(@Nullable Bundle savedInstanceState) {
17        super.onCreate(savedInstanceState);
18        setContentView(R.layout.activity_main);
19
20        this.setState(1);//错误,应该修改成this.setState(MyAnnotation.custom_1);
21    }
22}

上面举的例子是将@MyAnnotation和例子分开,如果想简单点可以都放在一起写,@MyAnnotation可以直接在Activity内部去定义。

实际上这个和开篇的引言一样,虽然编码的时候会提示error,但编译不影响,照样可以编译。在as中,还可以为注解在编码时候的错误切换severity等级。如下图所示。

as中切换severity等级 as中切换severity等级

CLASS

保留在class文件中,但是会被虚拟机忽略 ,即不能动态反射获取注解

字节码ASM

直接修改字节码Class文件以达到修改代码执行逻辑的目的 。AspectJ框架就是借助ASM来实现各自的功能 。

比如登录场景和未登录场景。一般都会通过if-else判断逻辑得到,但是存在判断的地方数量多就不太可行了。这个时候可以考虑用字节码增强方式。此时,我们可以借助AOP(面向切面)编程思想,将程序中所有功能点划分为:需要登录无需登录两种类型,即两个切面。对于切面的区分即可采用注解。具体有关AOP的内容,后续更新。

1@Target(ElementType.METHOD)
2@Retention(RetentionPolicy.CLASS)
3public @interface Login {
4}

可以通过@Login的注解判断是否跳转

1@Login
2public void jumpA(){
3startActivity(new Intent(this,AActivity.class));
4} 
5public void jumpB(){
6startActivity(new Intent(this,BActivity.class));
7}

ps:QQ空间通过字节码插桩,实现一段代码 在构造函数的插入。

插桩就是将一段代码插入到另一段代码,或替换另一段代码。字节码插桩顾名思义就是在我们编写的源码编译成字 节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。

before

 1public class MainActivity extends AppCompatActivity{
 2    public MainActivity(){}
 3    
 4    @InjectTime
 5    protected void oncreate(Bundle savedInstanceState){
 6        super.onCreate(savedInstanceState);
 7        this.setContentView(123456);
 8        this.a();
 9    }
10    
11    @InjectTime
12    void a(){
13        try{
14            Thread.sleep(2000L);         
15        }catch (InterruptedException e)
16        {
17            e.printStackTrace();
18        }
19    }
20}

after

 1public class MainActivity extends AppCompatActivity{
 2    public MainActivity(){}
 3    
 4    @InjectTime
 5    protected void oncreate(Bundle savedInstanceState){
 6        long time1 = System.currentTimeMillis();
 7        super.onCreate(savedInstanceState);
 8        this.setContentView(123456);
 9        this.a();
10        long time2 = System.currentTimeMillis();
11        System.out.println("oncreate execute time = "+(time2-time1));
12    }
13    
14    @InjectTime
15    void a(){
16        long time1 = System.currentTimeMillis();
17        try{
18            Thread.sleep(2000L);         
19        }catch (InterruptedException e)
20        {
21            e.printStackTrace();
22        }
23        long time2 = System.currentTimeMillis();
24        System.out.println("oncreate execute time = "+(time2-time1));
25    }
26}

上面有三个方法,mainactivity构造方法,oncreate方法,a方法

且只有oncreate和a方法上有注解@InjectTime,只有这两个方法需要加入计时的操作

插桩之后可以看到有注解的方法里面都增加了统计方法运行时间的操作逻辑。

RUNTIME

注解保留至运行期,意味着我们能够在运行期间结合反射技术获取注解中的所有信息

通过反射,动态的在运行期间对所注解的进行操作,这里举一个findViewById的例子,比较常见。

 1public static void injectView(Activity activity){
 2    Class<? extends Activity> cls = activity.getClass();
 3    //获得此类所有的成员
 4    //class获取属性的方式
 5    //getField获得自己和父类的成员(不包括private,只能是public)
 6    //getDecleredField只能获得自己的成员(不包括父类,所有作用域)
 7    //一般可用cls.getSuperclass().getDecleredField()获取父类所有属性
 8    Field[] declaredFields = cls.getDeclaredFields();
 9    for(Field field : declaredFields){
10        //判断属性是否被injectview注解声明
11        if(field.isAnnotationPresent(InjectView.class)){
12            InjectView injectView = filed.getAnnotation(InjectView.class);
13            //获得了注解中的设置id
14            int id = injectView.value();
15            View view = activity.findViewById(id);
16            //反射设置属性的值
17            filed.setAccessiable(true);//设置访问权限,允许操作private的属性
18            //filed就是具体的view对象比如tv,activity为限定的class类,view为tv设置的值,即tv=view
19            try{
20                filed.set(activity, view);
21            }catch (IllegalAccessException e)
22            {
23                e.printStackTrace();
24            }          
25        }
26    }
27}

这里的filed.set方法和method.invoke方法类似,第一个参数表示具体哪个对象上去设置的属性或者调用方法,这里的findViewById是Activity里面的方法,具体的Activity对象是注入的activity。如果这里注入的对象是一个静态的值,可以将第一个参数设置为null。

反射判断是否为静态变量判断静方法:filed.getModifiers()

主流的框架注入也是如此,不过目前ButterKnife框架也被废弃,目前越来越多的开发者使用上了ViewBinding。这些框架的核心都是以通过自定义注解+反射完成,ViewBinding是一个更轻量级、更纯粹的findViewById的替代方案,具体可点击

补充说明

Inherited

1@Retention(RetentionPolicy.RUNTIME)
2@Target({ElementType.METHOD,ElementType.TYPE})
3@Inherited
4public @interface MyAnnotation {
5    //用于身份校验,默认为真
6    boolean CheckInfo() default true;
7}

这里定义一个子类和父类,子类没有注解,而父类有注解

 1@MyAnnotation
 2public class PeterOne {
 3    public void IAMSPIDERMAN(){
 4        System.out.println("我是第一代蜘蛛侠");
 5    }
 6}
 7
 8public class PeterThree extends PeterOne{
 9    public void IAMSPIDERMAN(){
10        System.out.println("我是第三代蜘蛛侠");
11    }
12}

在操作子类对象的时候,可以通过反射的方式获取注解信息,用于逻辑判断

 1public static void main(String[] args) {
 2   try {
 3       //获取child的Class对象
 4       PeterThree Holland = new PeterThree();//第三版的荷兰第蜘蛛侠
 5       Class clazz = Holland.getClass();
 6       //获取该对象IAMSPIDERMAN方法上CheckInfo类型的注解
 7       MyAnnotation myAnnotation = (MyAnnotation) clazz.getAnnotation(MyAnnotation.class);
 8       if (myAnnotation.CheckInfo()) {
 9           System.out.println("Yes,i am peter");
10       } else {
11           System.out.println("Yes,i am not peter");
12       }
13   } catch (Exception e) {
14       e.printStackTrace();
15   }
16}

这里的子类通过反射获取父类的注解CheckInfo属性判断了是否为真的蜘蛛侠。

总结

级别 技术 说明
SOURCE APT和语法检查 在编译期能够获取注解与注解声明的类包括类中所有成员信息,一般用于生成额外的辅助类
CLASS 字节码增强 在编译出Class后,通过修改Class数据以实现修改代码逻辑目的
RUNTIME 反射 在程序运行期间,通过反射技术动态获取注解与其元素,从而完成不同的逻辑判定

参考

[1] pengjunlee, JAVA核心知识点–元注解详解, 2018.

[2] 我赌一包辣条, ButterKnife被弃用,ViewBinding才是findView的未来?, 2020.

[3] moon聊技术, Serv女朋友说想要自己的注解,我又活下来了!!!, 2021.

[4] f9q, 常用注解@Intdef与@Stringdef.2015.