Android 注解详细介绍

01.什么是注解

1.1 注解分类

  • 首先注解分为三类:
    • 标准 Annotation
      • 包括 Override, Deprecated, SuppressWarnings,是java自带的几个注解,他们由编译器来识别,不会进行编译, 不影响代码运行,至于他们的含义不是这篇博客的重点,这里不再讲述。
    • 元 Annotation
      • @Retention, @Target, @Inherited, @Documented,它们是用来定义 Annotation 的 Annotation。也就是当我们要自定义注解时,需要使用它们。
    • 自定义 Annotation
      • 根据需要,自定义的Annotation。而自定义的方式,下面我们会讲到。

1.2 自定义注解分类

  • 同样,自定义的注解也分为三类,通过元Annotation - @Retention 定义:
    • @Retention(RetentionPolicy.SOURCE)
      • 源码时注解,一般用来作为编译器标记。如Override, Deprecated, SuppressWarnings。
      • 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;源码注解(RetentionPolicy.SOURCE)的生命周期只存在Java源文件这一阶段,是3种生命周期中最短的注解。当在Java源程序上加了一个注解,这个Java源程序要由javac去编译,javac把java源文件编译成.class文件,在编译成class时会把Java源程序上的源码注解给去掉。需要注意的是,在编译器处理期间源码注解还存在,即注解处理器Processor 也能处理源码注解,编译器处理完之后就没有该注解信息了。
    • @Retention(RetentionPolicy.RUNTIME)
      • 运行时注解,在运行时通过反射去识别的注解。
      • 定义运行时注解,只需要在声明注解时指定@Retention(RetentionPolicy.RUNTIME)即可。
      • 运行时注解一般和反射机制配合使用,相比编译时注解性能比较低,但灵活性好,实现起来比较简答。
    • @Retention(RetentionPolicy.CLASS)
      • 编译时注解,在编译时被识别并处理的注解,这是本章重点。
      • 编译时注解能够自动处理Java源文件并生成更多的源码、配置文件、脚本或其他可能想要生成的东西。

1.3 实际注解案例

  • 实际注解案例
    • 运行时注解:retrofit
    • 编译时注解:Dagger2, ButterKnife, EventBus3

02.运行注解案例

2.1 创建一个注解

  • 如下所示

    1
    2
    3
    4
    5
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    public @interface ContentView {
    int value();
    }
  • 关于代码解释

    • 第一行:@Retention(RetentionPolicy.RUNTIME)
      • @Retention用来修饰这是一个什么类型的注解。这里表示该注解是一个运行时注解。这样APT就知道啥时候处理这个注解了。
    • 第二行:@Target({ElementType.TYPE})
      • @Target用来表示这个注解可以使用在哪些地方。比如:类、方法、属性、接口等等。这里ElementType.TYPE 表示这个注解可以用来修饰:Class, interface or enum declaration。当你用ContentView修饰一个方法时,编译器会提示错误。
    • 第三行:public @interface ContentView
      • 这里的interface并不是说ContentView是一个接口。就像申明类用关键字class。申明枚举用enum。申明注解用的就是@interface。(值得注意的是:在ElementType的分类中,class、interface、Annotation、enum同属一类为Type,并且从官方注解来看,似乎interface是包含@interface的)
      • /** Class, interface (including annotation type), or enum declaration */
      • TYPE,
    • 第四行:int value();
      • 返回值表示这个注解里可以存放什么类型值。比如我们是这样使用的
      • @ContentView(R.layout.activity_home)
      • R.layout.activity_home实质是一个int型id,如果这样用就会报错:
      • @ContentView(“string”)

2.2 注解解析

  • 注解申明好了,但具体是怎么识别这个注解并使用的呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @ContentView(R.layout.activity_test_video)
    public class TestActivity extends BaseActivity {

    //@ContentView(R.layout.activity_test_video) 这种使用是错误的
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    TextView tv_video = findViewById(R.id.tv_video);
    tv_video.setOnClickListener(v -> startActivity(
    new Intent(TestActivity.this,VideoActivity.class)));
    }

    }
  • 注解的解析就在BaseActivity中。来看一下BaseActivity代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //注解解析
    //遍历所有的子类
    for (Class c = this.getClass(); c != Context.class; c = c.getSuperclass()) {
    assert c != null;
    //找到修饰了注解ContentView的类
    ContentView annotation = (ContentView) c.getAnnotation(ContentView.class);
    if (annotation != null) {
    try {
    //获取ContentView的属性值
    int value = annotation.value();
    //调用setContentView方法设置view
    this.setContentView(value);
    } catch (RuntimeException e) {
    e.printStackTrace();
    }
    return;
    }
    }
    }
    }
  • 总结一下

    • 这是一个很简单的案例。现在对运行时注解的使用一定有了一些理解了。也知道了运行时注解被人呕病的地方在哪。你可能会觉得*setContentView(R.layout.activity_home)和@ContentView(R.layout.activity_home)*没什么区别,用了注解反而还增加了性能问题。

03.使用注解替代枚举

  • 代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 播放模式
    * -1 播放错误
    * 0 播放未开始
    * 1 播放准备中
    * 2 播放准备就绪
    * 3 正在播放
    * 4 暂停播放
    * 5 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
    * 6 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停
    * 7 播放完成
    */
    public @interface CurrentState{
    int STATE_ERROR = -1;
    int STATE_IDLE = 0;
    int STATE_PREPARING = 1;
    int STATE_PREPARED = 2;
    int STATE_PLAYING = 3;
    int STATE_PAUSED = 4;
    int STATE_BUFFERING_PLAYING = 5;
    int STATE_BUFFERING_PAUSED = 6;
    int STATE_COMPLETED = 7;
    }

04.使用注解限定类型

  • 代码如下所示

    • 枚举最大的作用是提供了类型安全。为了弥补Android平台不建议使用枚举的缺陷,官方推出了两个注解,IntDef和StringDef,用来提供编译期的类型检查。
    • 倘若,传入的值不是IjkPlayerType中的类型,则会导致编译提醒和警告。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
     /**
    * 通过注解限定类型
    * TYPE_IJK IjkPlayer,基于IjkPlayer封装播放器
    * TYPE_NATIVE MediaPlayer,基于原生自带的播放器控件
    */
    @Retention(RetentionPolicy.SOURCE)
    public @interface IjkPlayerType {
    int TYPE_IJK = 111;
    int TYPE_NATIVE = 222;
    }
    @IntDef({IjkPlayerType.TYPE_IJK,IjkPlayerType.TYPE_NATIVE})
    public @interface PlayerType{}


    //使用
    /**
    * 设置播放器类型,必须设置
    * 注意:感谢某人建议,这里限定了传入值类型
    * 输入值:ConstantKeys.IjkPlayerType.TYPE_IJK 或者 ConstantKeys.IjkPlayerType.TYPE_NATIVE
    * @param playerType IjkPlayer or MediaPlayer.
    */
    public void setPlayerType(@ConstantKeys.PlayerType int playerType) {
    mPlayerType = playerType;
    }