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

目录

  • 1 前言
  • 2 正文
    • 2.1 注解的作用
      • 可以使用注解做语法检查
      • 使用注解可以减少重复且易出错的样板代码
      • 使用注解可以在运行时获取一些配置信息
      • 使用注解可以生成帮助文档
    • 2.2 注解的语法
    • 2.3 使用注解
      • `@Target`为 `SOURCE` 的例子
      • `@Target`为 `CLASS` 的例子
      • `@Target`为 `RUNTIME` 的例子
  • 3 最后
  • 参考

1 前言

注解是在 Java SE5 引入进来的。注解在一定程度上是在把元数据与源代码结合在一起,而不是保存在外部文档中这一大的趋势之下所催生的。

因为笔者是作 Android 开发的,因此下面的介绍是偏于 Android 的实际应用。

本文会从以下几个方面来展开

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

2 正文

2.1 注解的作用

注解有什么作用呢?或者说,我们为什么要学习注解?从实际开发中应用的注解来给出答案吧。

可以使用注解做语法检查

@Override 这个注解大家都使用过,它表示当前的方法定义将覆盖超类中的方法。如果不小心拼写错误,或者方法签名对不上被覆盖的方法,编译器就会发出错误提示。请看下面的例子:

public class MyRunnable implements Runnable {

    // 这是正确的覆盖
    @Override
    public void run() {
    }

    // 这是错误的覆盖,但仍然使用了 @Override 注解
    @Override
    public void run2() {
    }
}

使用 javac 命令进行编译,得到结果:

com\example\annotationstudy\MyRunnable.java:9: 错误: 方法不会覆盖或实现超类型的方法
    @Override
    ^
1 个错误

其实,在 IDE 中可以看到第 9 行的 @Override 的地方底部有红色的波浪线,这是 IDE 进行的提示。这个提示和使用 javac 命令进行编译的提示是一模一样的。

需要说明的是,@Override 注解在 Java 中是可选择的,也就是说,可以写也可以不写。代码中第 4 行的 @Override 是可以不写的。但是,写上后可以增加程序的可读性,一眼就知道哪些方法是覆写的。

再举一个使用注解进行语法类型检查的例子:

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 对象:

public class PersonFactory {
    public List<Person> createPersonList() {
        List<Person> 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 注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}

@Test 注解的定义很像一个空的接口。不同之处有哪些呢?

  • 定义注解时, interface 前面必须加上 @ 符号;
  • 定义注解时,需要一些元注解:@Target@Retention
  • 定义注解时,注解的元素看起来很像接口的方法,但是可以给元素指定默认值。

正如接口可以没有任何方法一样(如 Serializable 接口),注解也可以没有任何元素,这样的注解被称为标记注解,例如上面定义的 @Test,以及 内置的@Override@Deprecated@FunctionalInterface@SafeVarargs注解。

Java 1.5 内置了一个有元素的注解:@SuppressWarnings 表示不当的编译器警告信息。

@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

注解是有限制的,具体来说如下:

  • 注解元素可以使用的类型有:
    • 所有基本类型(intfloatboolean 等)
    • String
    • Class
    • enum
    • Annotation,这说明注解可以嵌套
    • 以上类型的数组
  • 注解对默认值是有限制的:
    • 注解元素不能有不确定的值,要么指定默认值,要么在使用注解时提供元素的值。
    • 对于非基本类型的元素,无论在源代码中声明时,或是在注解接口中定义默认值时,都不能以 null 作为它的值。
  • 注解不支持继承,也就是说,不能使用关键字 extends 来继承某个注解。但是,所有的注解类型都继承于通用的 Annotation 接口,而这一点是不能显式地写出来的。

2.3 使用注解

定义好注解后,就需要着手使用了。如果不使用,注解和注释也没有什么区别。要使用注解,重要的就是创建与使用注解处理器。如何创建与使用注解处理器和定义注解时指定的@Retention 有很大关系。

下面是一个非常简单的注解处理器,用来查找某个类中哪些成员使用了 @Deprecated 注解。

VideoItem 类是一个使用了 @Deprecated 注解的类:

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 注解上指定的 @RetentionRetentionPolicy.RUNTIME,表示可以通过反射读取注解的信息。

因此,我们可以使用反射机制来查找被@Deprecated标记的成员,这样就知道了哪些成员是废弃的了。代码写在 DeprecatedTracker 里面:

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);
    }
}

打印信息如下:

deprecated field:private java.lang.String com.example.annotationstudy.VideoItem.objectId
deprecated 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 接口,而 ClassMethodField 以及 Constructor 等都实现了该接口。getAnnotation() 方法返回指定类型的注解对象,如果元素没有指定该类型的注解,则返回 nullisAnnotationPresent() 方法返回元素上是否有指定类型的注解。

上面是一个简单的例子,主要是为了快速展示注解处理器的创建与使用。下面我们会针对 @Target 的不同取值,分别给出实例。

@TargetSOURCE 的例子

这里有两种应用,一种是进行语法检查,一种是使用 APT 技术生成代码。

语法检查的例子

现在有一个音乐列表中包含两种条目类型,一是音乐条目,一是原生广告条目。使用 MusicItem 来说明:

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 对象的时候,可能有的同事会这样写:

public class MusicItemTest {
    public static void main(String[] args) {
        MusicItem musicItem = new MusicItem();
        musicItem.setType(2);
    }
}

注意,2 并不是合法的条目类型常量,但这时 IDE 并没有给出任何提示。

解决办法是定义 ItemType 注解,并在 getter/setter 方法上使用这个注解:

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 完全显示红色),提示信息为:

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 开始:

@IntRange(from = 1) int pageNumber = 0;

上面的声明就会语法检查不通过,提示:

Value must be ≥ 1 (was 0)

使用 APT 技术生成代码的例子

在实际开发中,页面中有大量控件时,去手动写出 findViewById() 的代码,是很累人的。不过,已经有很多优秀的框架,如 ButterKnifeViewBinding 可以解决这个问题。

这里我们实现一个类似于 ButterKnife 的方案。

不嫌啰嗦地,先看一下需要达到的目标,在一个页面 MainActivity 中不通过使用 findViewById() 的方式,获取到控件 id 对应的控件对象。最终的代码是:

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);
            }
        });
    }
}

运行效果:
Android筑基——深入理解注解的使用场景及实战_第1张图片
可以看到达到了我们的目标。接着,我们具体来看代码实现吧。

首先看一下目录结构:

它们之间的依赖关系如下:

bindview 主工程
bindview-annotations 注解模块 java lib
bindview-compiler 注解处理器模块 java lib
bindview-api 生成文件规则模块 android lib

现在有了整体的结构,大家不要觉得很复杂。我们这里的代码量很小,只是为了结构清晰才分成了这些模块。

好了,我们开始查看每一个部分。

先看 bindview-annotations 模块,这里只是定义了一个 @BindView 的注解:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
    @IdRes int value();
}

其中 @IdResandroidx.annotation 包下的,所以需要添加这个依赖:

implementation 'androidx.annotation:annotation:1.1.0'

接着看 :bindview-compiler 模块,这是注解处理器模块,通过它会帮助我们完成 findViewById() 的工作。

这个模块的依赖是:

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')
}

核心代码如下:

// 这个注解的作用是用来生成 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<TypeElement, List<Element>> map = new HashMap<>();

    /**
     * 文件生成器
     */
    private Filer filer;
    private String moduleName;
    private Map<String, String> 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<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        messager.printMessage(Diagnostic.Kind.NOTE, "process: set=" + set);
        if (!set.isEmpty()) {
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
            if (!elements.isEmpty()) {
                populateMap(elements);
                if (map.isEmpty()) {
                    return true;
                }
                //public class MainActivity$$BindView implements BindViewInterface {
                //    @Override
                //    public void bind(Object target) {
                //        MainActivity activity = (MainActivity) target;
                //        activity.tv = activity.findViewById(R.id.tv);
                //        activity.cl = activity.findViewById(R.id.cl);
                //    }
                //}
                TypeElement activityType = elementUtils.getTypeElement(ProcessorConstants.ACTIVITY_FULLNAME);
                TypeElement bindViewInterfaceType = elementUtils.getTypeElement(ProcessorConstants.BINDVIEWINTERFACE_FULLNAME);

                ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get("java.lang", "Object"), TARGET_ARGUMENT).build();
                for (Map.Entry<TypeElement, List<Element>> entry : map.entrySet()) {
                    TypeElement key = entry.getKey();
                    if (!typeUtils.isSubtype(key.asType(), activityType.asType())) {
                        messager.printMessage(Diagnostic.Kind.ERROR,
                                "@BindView can only be annotated in Activity");
                    } else {
                        ClassName className = ClassName.get(key);
                        BindViewFactory bindViewFactory = new BindViewFactory.Builder(parameterSpec)
                                .className(className)
                                .elementUtils(elementUtils)
                                .messager(messager)
                                .typeUtils(typeUtils)
                                .build();
                        bindViewFactory.addFirstStatement();
                        List<Element> elementList = entry.getValue();
                        for (Element element : elementList) {
                            bindViewFactory.buildStatement(element);
                        }
                        MethodSpec methodSpec = bindViewFactory.build();
                        TypeSpec typeSpec = TypeSpec.classBuilder(key.getSimpleName() + "$$BindView")
                                .addModifiers(Modifier.PUBLIC)
                                .addMethod(methodSpec)
                                .addSuperinterface(ClassName.get(bindViewInterfaceType))
                                .build();
                        JavaFile javaFile = JavaFile.builder(className.packageName(), typeSpec).build();

                        try {
                            javaFile.writeTo(filer);
                        } catch (IOException e) {
                            messager.printMessage(Diagnostic.Kind.ERROR, "create bindview file fail: " + e);
                        }
                    }
                }
            }
        }
        return false;
    }

    private void populateMap(Set<? extends Element> elements) {
        for (Element element : elements) {
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            if (map.containsKey(enclosingElement)) {
                map.get(enclosingElement).add(element);
            } else {
                ArrayList<Element> list = new ArrayList<>();
                list.add(element);
                map.put(enclosingElement, list);
            }
        }
    }

    private void parseOptions() {
        moduleName = options.get(ProcessorConstants.MODULE_NAME);
        packagenameForAPT = options.get(ProcessorConstants.PACKAGENAME_FOR_APT);
        messager.printMessage(Diagnostic.Kind.NOTE, "moduleName=" + moduleName +
                ",packagenameForAPT=" + packagenameForAPT);
        if (moduleName != null && packagenameForAPT != null) {
            messager.printMessage(Diagnostic.Kind.NOTE, "APT environment success");
        } else {
            messager.printMessage(Diagnostic.Kind.NOTE, "APT environment fail");
        }
    }
}

这时 Make Project,会在\bindview\build\generated\source\apt\debug\com\example\bindview 目录下生成 MainActivity$$BindView 文件:

public class MainActivity$$BindView implements BindViewInterface {
  public void bind(Object target) {
    MainActivity activity = (MainActivity) target;
    activity.tv = activity.findViewById(2131231081);
    activity.iv = activity.findViewById(2131230904);
    activity.btn = activity.findViewById(2131230807);
  }
}

可以看到在这里,进行了 findViewById() 的操作。接下来,就是如何使用这个文件了。

查看 bindview-api 模块,这里定义了生成文件的规则,即生成文件的接口,也定义了如何使用生成的文件,即 BindViewManager 类:

public class BindViewManager {
    private static volatile BindViewManager instance = null;

    private static final String BINDVIEW_FILE_NAME = "$$BindView";

    private final LruCache<String, BindViewInterface> cache;

    private BindViewManager() {
        cache = new LruCache<>(100);
    }

    public static BindViewManager getInstance() {
        if (instance == null) {
            synchronized (BindViewManager.class) {
                if (instance == null) {
                    instance = new BindViewManager();
                }
            }
        }
        return instance;
    }


    public void bind(Activity activity) {
        String activityFullName = activity.getClass().getName();
        BindViewInterface bindViewInterface = cache.get(activityFullName);
        if (bindViewInterface == null) {
            String bindViewInterfaceFileFullName = activityFullName + BINDVIEW_FILE_NAME;
            try {
                Class<?> clazz = Class.forName(bindViewInterfaceFileFullName);
                bindViewInterface = (BindViewInterface) clazz.newInstance();
                cache.put(activityFullName, bindViewInterface);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (bindViewInterface != null) {
            bindViewInterface.bind(activity);
        }
    }
}

使用这个类的代码在 MainActivity 中了。

这里只是介绍了部分代码,详细代码可以查看文章末尾的代码链接。

回到我们的主题,这里我们定义了一个 @Target(SOURCE)@BindView 注解,通过 apt 技术生成代码。那么,apt 是什么呢?为什么它可以帮助我们完成这样的工作?

apt 就是 annotation processing tool,注解处理工具,它专门用于操作 Java 源文件,而不是编译后的类。默认情况下,apt 会在处理完源文件后编译它们。

@TargetCLASS 的例子

这部分的应用是字节码增强技术。暂时还没有研究这块。

@TargetRUNTIME 的例子

这里仍然是使用 findViewById() 方法的例子,使用运行时注解来实现。

首先定义 @BindView 注解:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    @IdRes int value();
}

其次使用注解:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView tv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

然后编写注解处理器:

public class BindViewUtils {
    public static void bind(Activity activity) {
        Class<? extends Activity> activityClass = activity.getClass();
        Field[] declaredFields = activityClass.getDeclaredFields();
        for (Field field : declaredFields) {
            BindView annotation = field.getAnnotation(BindView.class);
            if (annotation != null) {
                int id = annotation.value();
                View view = activity.findViewById(id);
                field.setAccessible(true);
                try {
                    field.set(activity, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

最后使用注解处理器:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView tv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindViewUtils.bind(this);
        tv.setText("Hello, 2021!");
    }
}

运行效果如下:
Android筑基——深入理解注解的使用场景及实战_第2张图片

3 最后

代码已经全部上传到 github 上,地址为 AnnotationStudy。

参考

  • 轻松打造一个自己的注解框架

你可能感兴趣的:(Java,注解,使用场景)