注解我们在工作中都经常使用,很多面试题都会问到注解的一些特性,比如注解怎么设定修饰的对象,注解的保存策略等等,今天呢我们来聊一下注解的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修饰的注解
总结
通过本文,我们了解到了注解的保存策略的一些特性,不断的实践是巩固知识的最佳途径,希望这期博客对你有帮助