注解的Retention策略

328 阅读8分钟

注解我们在工作中都经常使用,很多面试题都会问到注解的一些特性,比如注解怎么设定修饰的对象,注解的保存策略等等,今天呢我们来聊一下注解的Retention策略以及在ART虚拟机中,不同的注解是如何被虚拟机所处理的

使用注解

这里我们拿Kotlin语言来演示,这里我们可以自定义几个针对Class的注解,一般我们只需要声明好Target以及Retention即可


@Target(AnnotationTarget.CLASS)
@Retention(SOURCE)
public annotation class SourceAnnotation



@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
public annotation class BinaryAnnotation

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
public annotation class RuntimeAnnotation

Retention 即注解的保存策略,目前有三个枚举值可以提供我们选择,分别是SOURCE,BINARY,RUNTIME

public enum class AnnotationRetention {
    /** Annotation isn't stored in binary output */
SOURCE,
    /** Annotation is stored in binary output, but invisible for reflection */
BINARY,
    /** Annotation is stored in binary output and visible for reflection (default retention) */
RUNTIME
}

这里我们再简单介绍一下它的含义

SOURCE :使用 SOURCE 保留策略的注解仅在源代码中存在,不会出现在编译后的字节码文件中。这类注解通常用于给开发者提供代码层面的提示、注释,或者用于代码生成工具,对编译和运行时没有实际影响。

BINARY:采用 BINARY 保留策略的注解会被保留在编译后的字节码文件中,但在运行时无法通过反射机制访问。

RUNTIME:使用 RUNTIME 保留策略的注解不仅会被保留在编译后的字节码文件中,而且在运行时可以通过反射机制访问。

一般针对注解的保存策略面试题达到这里算是合格了,但是我们接下来进一步看看,实际上它们是如何运作的以及在产物上的差别

这里我们定义三个测试类,分别用我们刚刚定义好的三个注解修饰

@SourceAnnotation
class MyTest1 {
}

@BinaryAnnotation
class MyTest2 {
}


@RuntimeAnnotation
class MyTest3 {
}

然后我们启动好app,我们分别使用getAnnotations方法查看上面三个类在运行时的情况

fun scanFun(){
    val annotation1 =  MyTest1::class.java.annotations
val annotation2 =  MyTest2::class.java.annotations
val annotation3 =  MyTest3::class.java.annotations
}

这个时候我们可以分别看到其结果

其中MyTest1 以及MyTest2都保留着一个metadata的注解,这是因为这两个类都是Kotlin的class,但是我们自定义的注解在运行时没有看到,MyTest3不仅存在着metadata的注解还保存着我们自定义的注解,这里的结论跟我们上面说到的八股文结论一致,那么接下来我们就要从不同的视角上看其运作机制

从字节码角度查看

我们可以把MyTest1,MyTest2,MyTest3分别按照字节码的角度查看

public final class com/example/annotation/MyTest1 {

  // compiled from: MyTest.kt

  @Lkotlin/Metadata;(mv={1, 7, 1}, k=1, xi=48, d1={"\u0000\u000c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0008\u0002\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002\u00a8\u0006\u0003"}, d2={"Lcom/example/annotation/MyTest1;", "", "()V", "app_debug"})
   ...
}
public final class com/example/annotation/MyTest2 {

  // compiled from: MyTest.kt

  @Lkotlin/Metadata;(mv={1, 7, 1}, k=1, xi=48, d1={"\u0000\u000c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0008\u0002\u0008\u0007\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002\u00a8\u0006\u0003"}, d2={"Lcom/example/annotation/MyTest2;", "", "()V", "app_debug"})

  @Lcom/example/annotation/BinaryAnnotation;() // invisible
  ...
  }
public final class com/example/annotation/MyTest3 {

  // compiled from: MyTest.kt

  @Lcom/example/annotation/RuntimeAnnotation;()

  @Lkotlin/Metadata;(mv={1, 7, 1}, k=1, xi=48, d1={"\u0000\u000c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0008\u0002\u0008\u0007\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002\u00a8\u0006\u0003"}, d2={"Lcom/example/annotation/MyTest3;", "", "()V", "app_debug"})
  ...
}

在字节码的角度中,我们验证了第一个结论,Retention策略为SOURCE注解修饰的类,我们的确在编译后的字节码文件中看不到修饰的注解,在编译后的产物中很好的被剔除了。

但是我们也留意到,被BINARY 跟RUNTIME注解修饰的类,其实在产物中是存在着注解信息的,那么这两个注解又有什么区别呢?通过编译后字节码可以看到,BINARY注解修饰的类会在注解后面添加了一个// invisible 的注释,但是我们在字节码的阶段其实没有太多的信息了,接下来再从运行时的角度上查看

从运行时角度查看

我们一开始通过getAnnotations 方法来或者类被修饰的注解数组

fun scanFun(){

    val annotation1 =  MyTest1::class.java.annotations
val annotation2 =  MyTest2::class.java.annotations
val annotation3 =  MyTest3::class.java.annotations

}

那么getAnnotations又是如何在运行时获取类修饰的注解信息呢?我们再看一下其实现

@Override
public Annotation[] getAnnotations() {
    // 先获取本身的类注解信息,
    HashMap<Class<?>, Annotation> map = new HashMap<Class<?>, Annotation>();
    for (Annotation declaredAnnotation : getDeclaredAnnotations()) {
        map.put(declaredAnnotation.annotationType(), declaredAnnotation);
    }
    //再获取父类以及其之上的注解信息
    for (Class<?> sup = getSuperclass(); sup != null; sup = sup.getSuperclass()) {
        for (Annotation declaredAnnotation : sup.getDeclaredAnnotations()) {
            Class<? extends Annotation> clazz = declaredAnnotation.annotationType();
            if (!map.containsKey(clazz) && clazz.isDeclaredAnnotationPresent(Inherited.class)) {
                map.put(clazz, declaredAnnotation);
            }
        }
    }

    /* Convert annotation values from HashMap to array. */
    Collection<Annotation> coll = map.values();
    return coll.toArray(new Annotation[coll.size()]);
}

我们可以从代码上看到,其实最终的产物是返回了类自身的注解信息以及父类(包括其继承之上的所有类)的注解信息,这里是获取每个类的注解信息是通过getDeclaredAnnotations方法实现的,而这个方法是一个native方法

@Override
@FastNative
public native Annotation[] getDeclaredAnnotations();

getDeclaredAnnotations是一个jni方法,其实现通过查找,我们可以看到对应的native方法是Class_getDeclaredAnnotations(jni方法可以通过静态以及动态注册的方式,如果我们想要查找某个jni方法的native实现,我们可以先找一下静态实现,对应的native方法为Class的名加上_后加上方法名即可,如果是动态注册我们可以看registernative方法所对应的方法即可)

static jobjectArray Class_getDeclaredAnnotations(JNIEnv* env, jobject javaThis) {
  ScopedFastNativeObjectAccess soa(env);
  StackHandleScope<1> hs(soa.Self());
  Handle<mirror::Class> klass(hs.NewHandle(DecodeClass(soa, javaThis)));
  if (klass->IsObsoleteObject()) {
    ThrowRuntimeException("Obsolete Object!");
    return nullptr;
  }
  if (klass->IsProxyClass() || klass->GetDexCache() == nullptr) {
    // Return an empty array instead of a null pointer.
    ObjPtr<mirror::Class>  annotation_array_class =
        WellKnownClasses::ToClass(WellKnownClasses::java_lang_annotation_Annotation__array);
    ObjPtr<mirror::ObjectArray<mirror::Object>> empty_array =
        mirror::ObjectArray<mirror::Object>::Alloc(soa.Self(),
                                                   annotation_array_class,
                                                   /* length= */ 0);
    return soa.AddLocalReference<jobjectArray>(empty_array);
  }
  return soa.AddLocalReference<jobjectArray>(annotations::GetAnnotationsForClass(klass));
}

这里我们的MyTest类最终会通过GetAnnotationsForClass方法获取返回内容并包装为jobjectArray返回上层,因为其java方法声明中是一个对象数组

ObjPtr<mirror::ObjectArray<mirror::Object>> GetAnnotationsForClass(Handle<mirror::Class> klass) {
  ClassData data(klass);
  const AnnotationSetItem* annotation_set = FindAnnotationSetForClass(data);
  return ProcessAnnotationSet(data, annotation_set, DexFile::kDexVisibilityRuntime);
}

GetAnnotationsForClass做了事情,第一是通过类获取到在编译后文件中的所有注解信息,并以AnnotationSetItem这个集合保存

无论是BINARY还是Runtime注解,因为最终都会在编译后存在,最终生成dex文件的时候,其实就包含了注解信息,因此AnnotationSetItem不仅包含了Runtime的注解,其实也包含了BINARY注解的信息。

因此呢,我们说BINARY修饰的注解在运行时无法查找,这个结论是不太正确的, 因为ART虚拟机是可以在运行时获取到BINARY注解的信息并且保存下来了,

那么为什么最终getAnnotations并没有返回BINARY注解的信息呢?这其实是为了满足Java虚拟机规范,java虚拟机规范中定义了RetentionPolicy为CLASS(kotlin中的BINARY)无法被反射或者被其他方式直接获取,但是具体怎么做其实根据虚拟机的不同而不同。

在ART中,其实这是接下来ProcessAnnotationSet所做的

每个注解都会在虚拟机内部解析dex文件后变为一个AnnotationItem结构体对象存在,这里就存在着BINARY与Runtime注解的最大不同,即visibility_,Runtime注解的visibility_为true,而BINARY的visibility_为false

struct AnnotationItem {
  uint8_t visibility_;
  uint8_t annotation_[1];

 private:
  DISALLOW_COPY_AND_ASSIGN(AnnotationItem);
};

因此ProcessAnnotationSet在最终的结果返回遍历中,其实是过滤了visibility_为false的注解,只把visibility_为true的注解返回给上层使用者,满足了最终的虚拟机规范

ObjPtr<mirror::ObjectArray<mirror::Object>> ProcessAnnotationSet(
    const ClassData& klass,
    const AnnotationSetItem* annotation_set,
    uint32_t visibility)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  const DexFile& dex_file = klass.GetDexFile();
 ... 
  uint32_t size = annotation_set->size_;
  Handle<mirror::ObjectArray<mirror::Object>> result(hs.NewHandle(
      mirror::ObjectArray<mirror::Object>::Alloc(self, annotation_array_class.Get(), size)));
  if (result == nullptr) {
    return nullptr;
  }

  uint32_t dest_index = 0;
  for (uint32_t i = 0; i < size; ++i) {
    const AnnotationItem* annotation_item = dex_file.GetAnnotationItem(annotation_set, i);
    // 这里是关键,如果不可见则查看下一个
    if (annotation_item->visibility_ != visibility) {
      continue;
    }
    const uint8_t* annotation = annotation_item->annotation_;
    ObjPtr<mirror::Object> annotation_obj = ProcessEncodedAnnotation(klass, &annotation);
    if (annotation_obj != nullptr) {
      result->SetWithoutChecks<false>(dest_index, annotation_obj);
      ++dest_index;
    } else if (self->IsExceptionPending()) {
      return nullptr;
    }
  }

   ....

  return trimmed_result;
}

在这里我们就清楚了,最终的结果中为什么不会存在BINARY修饰的注解

总结

通过本文,我们了解到了注解的保存策略的一些特性,不断的实践是巩固知识的最佳途径,希望这期博客对你有帮助