Android知识点整理15:注解(Annotation)

一、简介

注解是对程序信息的一种补充标记,本质上是一个特殊的接口,接口里面定义的方法实际上是注解的属性。单独使用注解没有任何意义,需要配合程序来使用。

就是一个元数据,即描述数据的数据。

1、注解和注释的区别

  •  定义不同

    • 注解:元数据,它是一种描述数据的数据。所以,可以说注解就是源代码的元数据。
    • 注释:是对源代码说明的文字
  • 作用对象不同

    • 注解:是给编译器看的。
    • 注释:是给人看的。
  • 书写范围不同

    • 注解:遵守一定的书写规范,以@开头,与工具一起使用
    • 注释:可以在代码的任何地方书写
  • 运行范围不同

    • 注解:可以参与编译器的任何阶段,对数据有一定的操作作用
    • 注释:被编译器忽略,不参与编译

2、Java文件的程序运行过程

注解的生命周期是和程序允许过程绑定的,所以有必要先了解Java文件的程序运行过程

  • Source(源代码阶段)

  • Class(类对象阶段)

  • Runtime(运行时)

 这个其实和元注解 @Retention 的生命周期对应

@Retention
  • 源码时注解(SOURCE):仅保留在源码阶段,在编译期就会被丢弃,一般是编译器来解析相关注解
  • 编译器注解(CLASS):保留在编译阶段,在类的加载阶段会被丢弃,一般使用APT技术来解析
  • 运行时注解(RUNTIME):全程保留,从源码到App运行过程中,一般使用反射来解析

特别注意:RUNTIME是包含CLASS和SOURCE,CLASS包含SOURCE

二、基础定义

1、元数据

注解上的注解

(1)、@Target

注解对象的作用范围

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package java.lang.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
    ElementType[] value();
}

public enum ElementType {
    TYPE,  /** Class, interface (including annotation type), or enum declaration */
    FIELD,   /** Field declaration (includes enum constants) */
    METHOD,  /** Method declaration */
    PARAMETER,     /** Formal parameter declaration */
    CONSTRUCTOR, /** Constructor declaration */
    LOCAL_VARIABLE,    /** Local variable declaration */
    ANNOTATION_TYPE, /** Annotation type declaration */
    PACKAGE, /** Package declaration */
    TYPE_PARAMETER, /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_USE;/**
     * Use of a type
     *
     * @since 1.8
     */

    private ElementType() {
    }
}

(2)、@ Retention

注解保留的生命周期

package java.lang.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Retention {
    RetentionPolicy value();
}

public enum RetentionPolicy {
    SOURCE, //源码级别
    CLASS,  //字节码级别
    RUNTIME; //运行时级别

    private RetentionPolicy() {
    }
}
  • 源码时注解(SOURCE):仅保留在源码阶段,在编译期就会被丢弃,一般是编译器来解析相关注解
  • 编译器注解(CLASS):保留在编译阶段,在类的加载阶段会被丢弃,一般使用APT技术来解析
  • 运行时注解(RUNTIME):全程保留,从源码到App运行过程中,一般使用反射来解析

(3)、 @Documented

作用在类上,被@Documented标记的类,使用javadoc命令执行一下对应的类就会生成文档

(4)、@Inherited

作用在子类上,被@Inherited标记的子类会继承父类的注解

2、注解的属性

实际注解的本质就是接口,接口可以定义方法,这些方法名称就是属性

(1)默认属性

注解默认的是value

 @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.SOURCE)
    public @interface ValueAnnotation {
        int value();
    }

 

@TestAnnotation.ValueAnnotation(123)
public class MainActivity extends AppCompatActivity

(2)其它自定义属性,不能直接传值,是要以key value的形式

 @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.SOURCE)
    public @interface ValueAnnotation {
        int value() ;
        String name() default "周瑜";
      int[] ids();
    }

 

@TestAnnotation.ValueAnnotation(@TestAnnotation.ValueAnnotation(value = 123,name="123",ids={1,2,3}))

 (3)、default

可以给属性赋默认值

三、作用

1、编译检查(主要是 SOURCE级别)

举几个例子就明白了

(1)、@Override

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

 这个Target目标是方法,Retention对应SOURCE级别

方法重载的作用主要是:检查是否正确重写了父类中的方法,表明这是一个重写的方法

同样类似的有

(2)、@LayoutRes

@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface LayoutRes {
}

可以指明 参数类型 必须是布局资源类型

ReportBrandAdapter(var context: Context, @LayoutRes layoutResId: Int)

还有 类似

(3)、@Nullable

@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})
public @interface Nullable {
}

 可以指明参数可以为null

ReportBrandAdapter(var context: Context, @Nullable layoutResId: Int)

 (4)、自己定义检查

public class TestAnnotation {

    private static final int Monday = 1;
    private static final int Sun = 2;
    
    @IntDef({Monday, Sun})
    @Retention(RetentionPolicy.SOURCE)
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    public @interface WeekAnnotation{

    }
    
    @WeekAnnotation
    private static int day;
    
    private static void setDay(@WeekAnnotation int day){
        TestAnnotation.day = day;
    }

    public static void main(String[] args) {
        setDay(Monday);
    }
}

这时候 setDay只能传入 Monday和Sun,不然会提示红色,但不影响编译和允许,就是IDE语法检查,在SOURCE级别

2、框架的应用,自动生成java代码,减轻开发者的工作量(APT工具的使用,编译时注解)

(1)ButterKnife

/**
 * Bind a field to the view for the specified ID. The view will automatically be cast to the field
 * type.
 * <pre><code>
 * {@literal @}BindView(R.id.title) TextView title;
 * </code></pre>
 */
@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}



上面的作用是绑定控件,减少findViewById,真正核心的不是这块。ButterKnife

ButterKnife.bind(this)

@NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
      return Unbinder.EMPTY;
    }

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InstantiationException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      }
      if (cause instanceof Error) {
        throw (Error) cause;
      }
      throw new RuntimeException("Unable to create binding instance.", cause);
    }
  }

主要有三步骤:

1.Class<?> targetClass = target.getClass(); 拿到传入的Activity的Class类对象
2.Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);寻找第一步的Class类对象的构造方法
3.return constructor.newInstance(target, source);//调用构造方法
这里我们还要从findBindingConstructorForClass(targetClass)跟进去看看他是怎么寻找的构造方法

@Nullable @CheckResult @UiThread
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); //重点一
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");//重点二
      //noinspection unchecked
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
      if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    } catch (NoSuchMethodException e) {
      throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

这里需要看的地方就两个,代码里已经加了注释
重点一:BINDINGS是一个Map对象,而且是个静态常量(生命周期等同于App),这个保证了我们如果是已经被绑定过的对象,不需要二次绑定,可以直接从Map里面取出来,主要是为了性能优化考虑。
重点二: cls.getClassLoader().loadClass(clsName + "_ViewBinding");这里以我们上面的例子为例,clsName就等于PosLoginActivity,这里就是用cls的类加载器去加载了一个名叫PosLoginActivity_ViewBinding的java类,下面我们就要找一下MainActivity_ViewBinding这个java类。上面我们提到过,Butterknife是通过APT技术来解析注解并生成相关代码,我们可以推断出这个PosLoginActivity_ViewBinding这个java类就是Butterknife生成的代码,通过APT生成的代码都在如图所示目录下

然后看PosLoginActivity_ViewBinding文件

 @UiThread
  public PosLoginActivity_ViewBinding(final PosLoginActivity target, View source) {
    this.target = target;

    View view;
    target.ll_clear_phone = Utils.findRequiredViewAsType(source, R.id.ll_clear_phone, "field 'll_clear_phone'", ImageView.class);
    target.cb_remember_account = Utils.findRequiredViewAsType(source, R.id.cb_remember_account, "field 'cb_remember_account'", CheckBox.class);
    target.cb_remember_password = Utils.findRequiredViewAsType(source, R.id.cb_remember_password, "field 'cb_remember_password'", CheckBox.class);
    view = Utils.findRequiredView(source, R.id.btn_login, "field 'btn_login' and method 'doLogin'");
    target.btn_login = Utils.castView(view, R.id.btn_login, "field 'btn_login'", Button.class);
    view7f0a0072 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.doLogin();
      }
    });

然后跟踪findRequireView,最终还是调用了source.findViewById(id)

 public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findRequiredView(source, id, who);
    return castView(view, id, who, cls);
  }

public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
      return cls.cast(view);
    } catch (ClassCastException e) {
      String name = getResourceEntryName(view, id);
      throw new IllegalStateException("View '"
          + name
          + "' with ID "
          + id
          + " for "
          + who
          + " was of the wrong type. See cause for more info.", e);
    }
  }

public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id);
    if (view != null) {
      return view;
    }
    String name = getResourceEntryName(source, id);
    throw new IllegalStateException("Required view '"
        + name
        + "' with ID "
        + id
        + " for "
        + who
        + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
        + " (methods) annotation.");
  }

我们发现点击事件会有比较重要的两步,第一步找到view,第二步类型转换,最后会设置点击事件,点击事件回调会调用 target.doLogin()

view = Utils.findRequiredView(source, R.id.btn_login, "field 'btn_login' and method 'doLogin'"); //第一步找到view
    target.btn_login = Utils.castView(view, R.id.btn_login, "field 'btn_login'", Button.class);//第二步类型转换

 view = Utils.findRequiredView(source, R.id.btn_login, "field 'btn_login' and method 'doLogin'");
      target.btn_login = Utils.castView(view, R.id.btn_login, "field 'btn_login'", Button.class);
      view7f0a0072 = view;
      view.setOnClickListener(new DebouncingOnClickListener() {
        @Override
        public void doClick(View p0) {
          target.doLogin();
        }
      });

注意:这里也解决了一个我们在使用Butterknife的疑问,就是我们使用注解的变量或者方法不能被private和protected修饰,因为我们的target的对象要直接调用对应的变量和方法,比如 target.textView ; target.doLogin();

看看如果用 private会报下面错误,权限错误

注解的保留期。

  • 源码时注解(SOURCE):仅保留在源码阶段,在编译期就会被丢弃,一般是编译器来解析相关注解
  • 编译器注解(CLASS):保留在编译阶段,在类的加载阶段会被丢弃,一般使用APT技术来解析
  • 运行时注解(RUNTIME):全程保留,从源码到App运行过程中,一般使用反射来解析

了解了前面的知识,我们就可以知道Butterknife肯定不是用反射来解析注解的,因为反射的效率比较低,也不是通过编译器来解析的,如果是这样的话,压根也就没有了Butterknife这个库了。Butterknife是用APT技术来解析注解的,APT的全称是Android Annotation Processor Tool,Android注解解析工具。它是在编译期阶段来解析注解并生成对应的代码,然后在使用的时候再去调生成的代码,这样就跟我们自己写好的代码自己调用一下,不会产生任何性能问题,并且会减少大量的模板类代码。

(2)ARouter也是应用APT技术

感兴趣的可以翻翻ARouter的源码

(3)APT技术

Android在编译期阶段会扫描所有继承自AbstractProcess的子类,并调用其中的process()方法,我们只要在process()方法里拿到我们所有的注解并生成对应的代码即可。下面我来介绍一下AbstractProcess以及它相关的几个方法
AbstractProcessor是一个抽象类,常用的方法有四个

  • init(ProcessingEnvironment processingEnv):注解处理器的初始化,一般在这里获取我们需要的工具类
  • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv):必须要重写的方法,也是实际入口有点类似与main()方法,我们要在这写我们实际的业务代码
  • getSupportedAnnotationTypes():指定注解处理器是注册给哪个注解的,返回指定支持的注解类集合
  • getSupportedSourceVersion():返回支持的jdk版本,一般都直接使用SourceVersion.latestSupported();

ButterKnifeProcessor这个类。
ButterKnifeProcessor中的源码很多,这里我们只看重点,直接到process方法中 

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);//解析所有的Butterknife注解并生成一个Map对象

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

      JavaFile javaFile = binding.brewJava(sdk, debuggable);//这里用到了JavaPoet库中的类,生成对应的java类
      try {
        javaFile.writeTo(filer);//把java类写入到文件里面
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

 Butterknife内部还引入了一个Java库叫JavaPoet,这个是专门用来编写java文件的。BindingSet这个类是对JavaPoet里面的一些属性的封装,它包含了一个Java文件里面包含的各种注解信息,由它生成java文件再写入即可。

Butterknife不存在性能问题是利用了APT技术,在代码编译阶段就对注解做了解析并生成了相关代码,Butterknife.bind(this);实际上就是对生成代码的调用。

3、根据注解生成帮助文档

主要使用 @Documented 元注解,被@Documented的类  使用javadoc命令执行一下对应的类就会生成文档

4、字节码增强技术(主要在Class级别)

比如要统计带@InjectTime 的注解 的方法执行时间,就要修改Class,所以是Class级别

(1)、插桩技术

5、运行时注解作用(反射)

早期的 AndroidAnnotation框架,AFinal框架

(1)模仿旧的的ButterKnife使用反射

package com.androidtv.pos.annotation;

import androidx.annotation.IdRes;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

 

package com.androidtv.pos.annotation;

import android.app.Activity;
import android.view.View;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class InjectUtil {
    public static void injectView(Activity activity)  {
        Class<? extends Activity> cls = activity.getClass();
        try {
            Method method = cls.getMethod("onClick", Integer.TYPE); //得到方法
            method.setAccessible(true);

            Method[] methods = cls.getMethods();
            Field field = cls.getField("name"); //获得此类和父类所有公共成员
            Field declareField = cls.getDeclaredField("name"); //得到次类所有成员
            //如果要拿到父类所有成员
            Field[] superField = cls.getSuperclass().getDeclaredFields();
            Field[] declaredFields = cls.getDeclaredFields();
            for(Field decField : declaredFields){
                if(decField.isAnnotationPresent(InjectView.class)){
                    InjectView injectView = field.getAnnotation(InjectView.class);
                    int id = injectView.value();
                    View view = activity.findViewById(id);
                    decField.setAccessible(true);
                    decField.set(activity, view);
                }
            }
        }catch (Exception e){

        }
    }
}
 @InjectView(R.id.tv_title)
    private TextView tv_title;

(2)、 通过反射拿到泛型的真实类型

 

参考文章:https://www.cnblogs.com/hahayixiao/p/13772011.html#jieandshi

                 https://www.jianshu.com/p/6c06568b2919



 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值