请选择 进入手机版 | 继续访问电脑版

Android筑基——深入理解注解的使用场景及实战

[复制链接]
小甜心 发表于 2021-1-1 18:31:41 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
目次



1 前言

注解是在 Java SE5 引入进来的。注解在一定程度上是在把元数据与源代码联合在一起,而不是生存在外部文档中这一大的趋势之下所催生的。
因为笔者是作 Android 开发的,因此下面的先容是偏于 Android 的实际应用。
本文会从以下几个方面来展开


  • 注解有什么作用?主要是从实际开发中的使用入手来进行说明。
  • 注解如何界说以及有哪些需要注意的地方?这部分是语法说明。
  • 注解上界说的差别的生存计谋,即@Retention 的差别取值到底有什么区别?这部分是本文的重点所在,会给出实例进行说明。
2 正文

2.1 注解的作用

注解有什么作用呢?大概说,我们为什么要学习注解?从实际开发中应用的注解来给出答案吧。
可以使用注解做语法查抄

@Override 这个注解各人都使用过,它表示当前的方法界说将覆盖超类中的方法。如果不小心拼写错误,大概方法签名对不上被覆盖的方法,编译器就会发堕落误提示。请看下面的例子:
  1. public class MyRunnable implements Runnable {    // 这是正确的覆盖    @Override    public void run() {    }    // 这是错误的覆盖,但仍然使用了 @Override 注解    @Override    public void run2() {    }}
复制代码
使用 javac 下令进行编译,得到效果:
  1. com\example\annotationstudy\MyRunnable.java:9: 错误: 方法不会覆盖或实现超范例的方法    @Override    ^1 个错误
复制代码
实在,在 IDE 中可以看到第 9 行的 @Override 的地方底部有赤色的波浪线,这是 IDE 进行的提示。这个提示和使用 javac 下令进行编译的提示是一模一样的。
需要说明的是,@Override 注解在 Java 中是可选择的,也就是说,可以写也可以不写。代码中第 4 行的 @Override 是可以不写的。但是,写上后可以增加程序的可读性,一眼就知道哪些方法是覆写的。
再举一个使用注解进行语法范例查抄的例子:
  1. package com.example.annotationstudy;import androidx.annotation.DrawableRes;import androidx.annotation.StringRes;public class Person {    @DrawableRes    private int avatarResId;    @StringRes    private int nameResId;    // @DrawableRes 注解表示期望这个 int 值是一个图片资源类的 id    // @StringRes 注解表示期望这个 int 值是一个 String 资源的 id    public Person(@DrawableRes int avatarResId, @StringRes int nameResId) {        this.avatarResId = avatarResId;        this.nameResId = nameResId;    }}
复制代码
在 PersonFactory 里创建一些 Person 对象:
  1. public class PersonFactory {    public List createPersonList() {        List result = new ArrayList();        // 编译正确        result.add(new Person(R.mipmap.ic_launcher, R.string.name_peter));        // 在 IDE 中可以传入的实参都酿成赤色,编译堕落,        // 参数 1 提示:Expected resource of type drawable        // 参数 2 提示:Expected resource of type string        result.add(new Person(1, 1));        return result;    }}
复制代码
使用注解可以淘汰重复且易堕落的样板代码

这里的实例是 Android 中的 Room 数据库的使用。这里不再展开说明。可以参考文档:https://developer.android.google.cn/training/data-storage/room
使用注解可以在运行时获取一些设置信息

这里的实例是 Retrofit 中的注解,在运行时获取设置的请求方式,请求地址等信息。
使用注解可以生成资助文档

通过给Annotation注解加上 @Documented 标签,能使该Annotation标签出现在 javadoc 中。
这几个作用里面,哪个最能吸引你呢?。
2.2 注解的语法

这里界说一个 @Test 注解:
  1. @Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Test {}
复制代码
@Test 注解的界说很像一个空的接口。差别之处有哪些呢?


  • 界说注解时, interface 前面必须加上 @ 符号;
  • 界说注解时,需要一些元注解:@Target 和 @Retention;
  • 界说注解时,注解的元素看起来很像接口的方法,但是可以给元素指定默认值。
正如接口可以没有任何方法一样(如 Serializable 接口),注解也可以没有任何元素,这样的注解被称为标志注解,比方上面界说的 @Test,以及 内置的@Override ,@Deprecated,@FunctionalInterface,@SafeVarargs注解。
Java 1.5 内置了一个有元素的注解:@SuppressWarnings 表示不妥的编译器告诫信息。
  1. @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})@Retention(RetentionPolicy.SOURCE)public @interface SuppressWarnings {    String[] value();}
复制代码
什么是元注解呢?元注解就是专门负责注解其他注解的注解,大概说专门负责新注解创建的注解。
在 Java SE5 中有 4 种元注解:@Target,@Retention,@Documented 和 @Inherited。
@Target 表示该注解可以用于什么地方。可能的 ElementType 参数包罗:


  • CONSTRUCTOR:用于构造器的声明;
  • FIELD:用于域声明(包罗 enum 实例);
  • LOCAL_VARIABLE:用于局部变量的声明;
  • METHOD:用于方法的声明;
  • PACKAGE:用于包的声明;
  • PARAMETER:用于平凡参数的声明;
  • TYPE:用于类、接口(包罗注解范例)或 enum 声明;
  • ANNOTATION_TYPE:用于注解的声明;
  • TYPE_PARAMETER:这是 Java 1.8 参加的,用于范例参数的声明;
  • TYPE_USE:这是 Java 1.8 参加的,用于一个范例的使用。
在 @Target 注解中指定的每一个 ElementType 就是一个约束,它告诉编译器,这个自界说的注解可以用于哪个范例或哪些范例。指定多个范例时,需要以逗号分隔并写在花括号{}里面,比方 @Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})。如果不写 @Target 注解,表示该注解可以用于所有的 ElementType。
@Retention 表示需要在什么级别生存该注解信息。可选的 RetentionPolicy 参数包罗:


  • SOURCE:表示注解仅在源码中可用,将会被编译器丢掉;
  • CLASS :表示注解会被编译器记载在 class 文件中,但在运行时虚拟机(VM)不会生存注解。这也是默认的行为。
  • RUNTIME:表示注解会被编译器记载在 class 文件中,而且在运行时虚拟机(VM)会生存注解。所以这里可以通过反射读取注解的信息。可以参考:java.lang.reflect.AnnotatedElement。
注解是有限制的,具体来说如下:


  • 注解元素可以使用的范例有:

    • 所有根本范例(int、float、boolean 等)
    • String
    • Class
    • enum
    • Annotation,这说明注解可以嵌套
    • 以上范例的数组

  • 注解对默认值是有限制的:

    • 注解元素不能有不确定的值,要么指定默认值,要么在使用注解时提供元素的值。
    • 对于非根本范例的元素,无论在源代码中声明时,或是在注解接口中界说默认值时,都不能以 null 作为它的值。

  • 注解不支持继承,也就是说,不能使用关键字 extends 来继承某个注解。但是,所有的注解范例都继承于通用的 Annotation 接口,而这一点是不能显式地写出来的。
2.3 使用注解

界说好注解后,就需要着手使用了。如果不使用,注解和注释也没有什么区别。要使用注解,重要的就是创建与使用注解处置惩罚器。如何创建与使用注解处置惩罚器和界说注解时指定的@Retention 有很大关系。
下面是一个非常简朴的注解处置惩罚器,用来查找某个类中哪些成员使用了 @Deprecated 注解。
VideoItem 类是一个使用了 @Deprecated 注解的类:
  1. public class VideoItem {    @Deprecated    private String objectId;    private String videoId;    private String videoUrl;    @Deprecated    public VideoItem(String objectId) {        this.objectId = objectId;    }    public VideoItem() {    }    @Deprecated    public String getObjectId() {        return objectId;    }    @Deprecated    public void setObjectId(String objectId) {        this.objectId = objectId;    }}
复制代码
看一下,@Deprecated 用在成员变量,构造方法,以及成员方法上。查察 @Deprecated 注解上指定的 @Retention 是 RetentionPolicy.RUNTIME,表示可以通过反射读取注解的信息。
因此,我们可以使用反射机制来查找被@Deprecated标志的成员,这样就知道了哪些成员是废弃的了。代码写在 DeprecatedTracker 里面:
  1. public class DeprecatedTracker {    public static void trackDeprecated(Class cl) {        // 查找成员变量        Field[] declaredFields = cl.getDeclaredFields();        for (Field field : declaredFields) {            if (field.getAnnotation(Deprecated.class) != null) {                System.out.println("deprecated field:" + field);            }        }        // 查找成员方法        Method[] declaredMethods = cl.getDeclaredMethods();        for (Method method : declaredMethods) {            if (method.isAnnotationPresent(Deprecated.class)) {                System.out.println("deprecated method:" +  method);            }        }        // 查找构造方法        Constructor[] declaredConstructors = cl.getDeclaredConstructors();        for (Constructor constructor : declaredConstructors) {            if (constructor.isAnnotationPresent(Deprecated.class)) {                System.out.println("deprecated constructor:" + constructor);            }        }    }    public static void main(String[] args) {        DeprecatedTracker.trackDeprecated(VideoItem.class);    }}
复制代码
打印信息如下:
  1. deprecated field:private java.lang.String com.example.annotationstudy.VideoItem.objectIddeprecated method:public java.lang.String com.example.annotationstudy.VideoItem.getObjectId()deprecated method:public void com.example.annotationstudy.VideoItem.setObjectId(java.lang.String)deprecated constructor:public com.example.annotationstudy.VideoItem(java.lang.String)
复制代码
需要说明的是,上面例子中使用到的 getAnnotation() 和 isAnnotationPresent() 方法属于 AnnotatedElement 接口,而 Class、Method、Field 以及 Constructor 等都实现了该接口。getAnnotation() 方法返回指定范例的注解对象,如果元素没有指定该范例的注解,则返回 null。isAnnotationPresent() 方法返回元素上是否有指定范例的注解。
上面是一个简朴的例子,主要是为了快速展示注解处置惩罚器的创建与使用。下面我们会针对 @Target 的差别取值,分别给出实例。
@Target为 SOURCE 的例子

这里有两种应用,一种是进行语法查抄,一种是使用 APT 技能生成代码。
语法查抄的例子
现在有一个音乐列表中包罗两种条目范例,一是音乐条目,一是原生广告条目。使用 MusicItem 来说明:
  1. public class MusicItem {    public static final int ITEM_TYPE_MUSIC = 0;    public static final int ITEM_TYPE_AD = 1;    private int type;    public int getType() {        return type;    }    public void setType(int type) {        this.type = type;    }}
复制代码
在构造 MusicItem 对象的时候,可能有的同事会这样写:
  1. public class MusicItemTest {    public static void main(String[] args) {        MusicItem musicItem = new MusicItem();        musicItem.setType(2);    }}
复制代码
注意,2 并不是正当的条目范例常量,但这时 IDE 并没有给出任何提示。
管理办法是界说 ItemType 注解,并在 getter/setter 方法上使用这个注解:
  1. public class MusicItem {    @IntDef({ITEM_TYPE_MUSIC, ITEM_TYPE_AD})    @Retention(RetentionPolicy.SOURCE)    @interface ItemType {    }    public static final int ITEM_TYPE_MUSIC = 0;    public static final int ITEM_TYPE_AD = 1;    private int type;    public @ItemType int getType() {        return type;    }    public void setType(@ItemType int type) {        this.type = type;    }}
复制代码
然后可以看到在 musicItem.setType(2); 处已经有了语法查抄(在 AS 4.1.1 中 2 完全显示赤色),提示信息为:
  1. Must be one of: MusicItem.ITEM_TYPE_MUSIC, MusicItem.ITEM_TYPE_AD
复制代码
这个例子不是很复杂,但是有些地方需要做一下说明:


  • @IntDef 是由 Android 界说好的一个元注解,位置是在 androidx.annotation 包里面,表示被注解的整型元素代表了一个逻辑范例而且它的值应该是显着声明的常量。
  • 注解的元素在使用时体现为名-值对的形式,但是我们这里使用的 @IntDef 却是 @IntDef({ITEM_TYPE_MUSIC, ITEM_TYPE_AD}),又不报错,这是什么原因呢?这实在是使用了快捷方式,即在注解中界说了名为 value 的元素,而且在用于该注解的时候,如果该元素是唯一需要赋值的一个元素,那么此时无需使用名-值对这种语法,而只需在括号内给出 value 元素的值即可。
在 androidx.annotation 包下,有许多资助我们进行语法查抄的注解,各人可以自行探索。这里我们不嫌啰嗦,再举一个 @IntRange 的使用。
在做分页请求时,有的配景给出页面从 0 开始,有的从 1 开始。这个必须要搞好,不然要么少么少请求一页数据,要么就啥也请求不到,给开发大概业务带来贫苦。
而使用 @IntRange 可以管理这个问题,进行配景接口接入的时候可以界说好是从 0 开始照旧从 1 开始:
  1. @IntRange(from = 1) int pageNumber = 0;
复制代码
上面的声明就会语法查抄不通过,提示:
  1. Value must be ≥ 1 (was 0)
复制代码
使用 APT 技能生成代码的例子
在实际开发中,页面中有大量控件时,去手动写出 findViewById() 的代码,是很累人的。不外,已经有许多优秀的框架,如 ButterKnife,ViewBinding 可以管理这个问题。
这里我们实现一个雷同于 ButterKnife 的方案。
不嫌啰嗦地,先看一下需要到达的目标,在一个页面 MainActivity 中不通过使用 findViewById() 的方式,获取到控件 id 对应的控件对象。最终的代码是:
  1. public class MainActivity extends AppCompatActivity {    @BindView(R.id.tv)    TextView tv;    @BindView(R.id.iv)    ImageView iv;    @BindView(R.id.btn)    Button btn;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        BindViewManager.getInstance().bind(this);        tv.setText("Happy New Year!");        iv.setImageResource(R.mipmap.ic_launcher);        btn.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                SecondActivity.start(MainActivity.this);            }        });    }}
复制代码
运行效果:
[img=30%,30%]https://img-blog.csdnimg.cn/20201231064847831.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dpbGx3YXlfd2FuZw==,size_16,color_FFFFFF,t_70#pic_center[/img]

可以看到到达了我们的目标。接着,我们具体来看代码实现吧。
首先看一下目次结构:

它们之间的依赖关系如下:
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    bindview 主工程                                                                                      bindview-annotations 注解模块 java lib                                                                                      bindview-compiler 注解处置惩罚器模块 java lib                                                                                      bindview-api 生成文件规则模块 android lib                                              现在有了整体的结构,各人不要以为很复杂。我们这里的代码量很小,只是为了结构清晰才分成了这些模块。
好了,我们开始查察每一个部分。
先看 bindview-annotations 模块,这里只是界说了一个 @BindView 的注解:
  1. @Target(ElementType.FIELD)@Retention(RetentionPolicy.SOURCE)public @interface BindView {    @IdRes int value();}
复制代码
此中 @IdRes 是 androidx.annotation 包下的,所以需要添加这个依赖:
  1. implementation 'androidx.annotation:annotation:1.1.0'
复制代码
接着看 :bindview-compiler 模块,这是注解处置惩罚器模块,通过它会资助我们完成 findViewById() 的工作。
这个模块的依赖是:
  1. dependencies {    compileOnly 'com.google.auto.service:auto-service:1.0-rc4'    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'    // 用于资助我们通过类调用的形式来生成Java代码    implementation 'com.squareup:javapoet:1.9.0'    implementation project(':bindview-annotations')}
复制代码
核心代码如下:
[code]// 这个注解的作用是用来生成 META-INF/javax.annotation.processing.Processor 这个文件,文件里就是// 注解处置惩罚器的全路径,这个文件会被 ServiceLoader 类使用,用于加载注解服务。@AutoService(Processor.class)// 指定注解处置惩罚器支持的 JDK 编译版本@SupportedSourceVersion(SourceVersion.RELEASE_7)// 指定注解处置惩罚器支持处置惩罚的注解@SupportedAnnotationTypes({ProcessorConstants.BINDVIEW_FULLNAME})@SupportedOptions({ProcessorConstants.MODULE_NAME, ProcessorConstants.PACKAGENAME_FOR_APT})public class BindViewProcessor extends AbstractProcessor {    /**     * 利用 Element 的工具类(类,函数,属性,枚举,构造方法都是 Element)     */    private Elements elementUtils;    /**     * 打印日志类     */    private Messager messager;    /**     * 用来对范例进行利用的实用工具方法     */    private Types typeUtils;    /**     * 按 Activity 存放使用了 @BindView 注解的集合     * 键是 Activity     * 值是使用了 @BindView 的元素列表     */    private Map map = new HashMap();    /**     * 文件生成器     */    private Filer filer;    private String moduleName;    private Map options;    private String packagenameForAPT;    @Override    public synchronized void init(ProcessingEnvironment processingEnvironment) {        super.init(processingEnvironment);        elementUtils = processingEnvironment.getElementUtils();        messager = processingEnvironment.getMessager();        typeUtils = processingEnvironment.getTypeUtils();        filer = processingEnvironment.getFiler();        options = processingEnvironment.getOptions();        parseOptions();    }    @Override    public boolean process(Set
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

发布主题

专注素材教程免费分享
全国免费热线电话

18768367769

周一至周日9:00-23:00

反馈建议

27428564@qq.com 在线QQ咨询

扫描二维码关注我们

Powered by Discuz! X3.4© 2001-2013 Comsenz Inc.( 蜀ICP备2021001884号-1 )