问题
有一个需求是要将传入的数据结构用GSON序列化存入数据库, 为了避免被混淆,需要使用 @android.support.annotation.Keep 注解来标记存入的数据结构。
因此我在代码里做了一个判断,如果这个类没有使用Keep注解就在Debug时抛出异常:
if (Logger.debug()) {
if (!clazz.isAnnotationPresent(Keep.class)) {
throw new IllegalArgumentException("DataBaseManager: Data type " + clazz.getName() + " must be annotated by @Keep !");
}
}
在我的测试机上运行正常(Nexus 5,Android 6.0.1),然而在测试过程中发现,一台 4.3 的手机抛出了异常。
在查找问题的过程中发现,@Keep 这个注解的作用域(RetentionPolicy)是CLASS级别的,也就是说,理论上所有版本的系统在运行时都是取不到这个注解的,都应该抛出异常才对,但是为什么我的手机没有呢??先写一个小 Demo 来测试一下:
Demo 测试
首先定义三个注解:
@Retention(SOURCE)
public @interface Source {
}
@Retention(CLASS)
public @interface Clazz {
}
@Retention(RUNTIME)
public @interface Runtime {
}
然后定义三个类:
@Source
public class A {
}
@Source
@Clazz
public class B {
}
@Source
@Clazz
@Runtime
public class C {
}
然后使用Class中的方法来读取类中的注解:
Annotation[] as = A.class.getDeclaredAnnotations();
Log.d("AnnotationDebug", "A Annotations length = " + as.length);
for (Annotation a : as) {
Log.d("AnnotationDebug", "A : " + a.toString());
}
Annotation[] bs = B.class.getDeclaredAnnotations();
Log.d("AnnotationDebug", "B Annotations length = " + bs.length);
for (Annotation b : bs) {
Log.d("AnnotationDebug", "B : " + b.toString());
}
Annotation[] cs = C.class.getDeclaredAnnotations();
Log.d("AnnotationDebug", "C Annotations length = " + cs.length);
for (Annotation c : cs) {
Log.d("AnnotationDebug", "C : " + c.toString());
}
Log.d("AnnotationDebug", "C @Source presentation: " + (C.class.isAnnotationPresent(Source.class) ? "true" : "false"));
Log.d("AnnotationDebug", "C @Clazz presentation: " + (C.class.isAnnotationPresent(Clazz.class) ? "true" : "false"));
Log.d("AnnotationDebug", "C @Runtime presentation: " + (C.class.isAnnotationPresent(Runtime.class) ? "true" : "false"));
最后分别在4.3、5.0、6.0、7.1、8.0的机器上运行,结果…很魔幻:
系统版本 | 4.3 | 5.0 | 6.0 | 7.1 | 8.0 |
---|---|---|---|---|---|
getDeclaredAnnotations() | 0-0-1 | 0-0-1 | 0-0-1 | 0-0-1 | 0-0-1 |
isAnnotationPresent(Source.class) | false | false | false | false | false |
isAnnotationPresent(Clazz.class) | false | true | true | false | false |
isAnnotationPresent(Runtime.class) | true | true | true | true | true |
可以看到,getDeclaredAnnotations() 方法返回的注解在不同版本上都是正确的,确实只有类 C 中存在一个 Runtime 注解。
但是在不同版本上,isAnnotationPresent() 方法返回的结果却各不相同,5.0 和 6.0 版本在判断 CLASS 级别的注解时均会返回 True!
5.x/6.x 源码探究
问题出在6.0(API 23)上,那就先看看 6.0 的 Class.java 是怎么实现的:
@Override
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
return AnnotationAccess.isAnnotationPresent(this, annotationType);
}
可以看到,6.0版本的 Class.isAnnotationPresent() 方法委托给了 AnnotationAccess这个类实现,而 AnnotationAccess 这个类不在SDK内,无法用 Android Studio 直接查看,那就去官网看这个类的源码:
https://android.googlesource.com/platform/libcore/+/android-6.0.1_r79/luni/src/main/java/libcore/reflect/AnnotationAccess.java
这个方法的实现路径依次为:
- Class.isAnnotationPresent()
- AnnotationAccess.isAnnotationPresent()
- AnnotationAccess.isDeclaredAnnotationPresent()
- AnnotationAccess.getAnnotation()
最终在下面的方法里面去尝试取这个Annotation:
private static com.android.dex.Annotation getAnnotation(
AnnotatedElement element, Class<? extends Annotation> annotationClass) {
int annotationSetOffset = getAnnotationSetOffset(element);
if (annotationSetOffset == 0) {
return null; // no annotation
}
Class<?> dexClass = getDexClass(element);
Dex dex = dexClass.getDex();
Dex.Section setIn = dex.open(annotationSetOffset); // annotation_set_item
String annotationInternalName = InternalNames.getInternalName(annotationClass);
for (int i = 0, size = setIn.readInt(); i < size; i++) {
int annotationOffset = setIn.readInt();
Dex.Section annotationIn = dex.open(annotationOffset); // annotation_item
// The internal string name of the annotation is compared here and deliberately not
// the value of annotationClass.getTypeIndex(). The annotationClass may have been
// defined by a different dex file, which would make the indexes incomparable.
com.android.dex.Annotation candidate = annotationIn.readAnnotation();
String candidateInternalName = dex.typeNames().get(candidate.getTypeIndex());
if (candidateInternalName.equals(annotationInternalName)) {
return candidate;
}
}
return null; // This set doesn't contain the annotation.
}
看到这里简直惊呆了,从Dex文件里面直接读取这个类的注解,然后直接返回,没有任何判断!问题是,Dex文件里面的不仅包含 Runtime 级别的注解,也包含 CLASS 级别的注解,也就是说,使用 Class.isAnnotationPresent() 方法来判断,CLASS 级的注解也会返回 True
我们再来看看getDeclaredAnnotations()方法的实现:
- Class.getDeclaredAnnotations()
- AnnotationAccess.getDeclaredAnnotations()
- AnnotationAccess.annotationSetToAnnotations()
最后的实现过程:
private static List<Annotation> annotationSetToAnnotations(Class<?> context, int offset) {
if (offset == 0) {
return Collections.emptyList(); // no annotations in the set
}
Dex dex = context.getDex();
Dex.Section setIn = dex.open(offset); // annotation_set_item
int size = setIn.readInt();
List<Annotation> result = new ArrayList<Annotation>(size);
for (int i = 0; i < size; i++) {
int annotationOffset = setIn.readInt();
Dex.Section annotationIn = dex.open(annotationOffset); // annotation_item
com.android.dex.Annotation annotation = annotationIn.readAnnotation();
if (annotation.getVisibility() != VISIBILITY_RUNTIME) {
continue;
}
Class<? extends Annotation> annotationClass =
getAnnotationClass(context, dex, annotation.getTypeIndex());
if (annotationClass != null) {
result.add(toAnnotationInstance(context, dex, annotationClass, annotation.getReader()));
}
}
return result;
}
注意这个方法的第13行,在转换Annotation的时候判断了一下,将不是RUNTIME的注解被过滤掉了,因此最后的结果是没有问题的
5.0 系统的实现和 6.0 是一致的,均是委托给 AnnotationAccess 这个类,而且都在判断的时候少了一个过滤,导致了最后的结果不正确
4.3 源码探究
再看 4.3 系统的源码:
https://android.googlesource.com/platform/libcore/+/android-4.3.1_r1/luni/src/main/java/java/lang/Class.java
Class.isAnnotationPresent() 方法最终的实现是一个 Native 的方法:
/**
* Returns true if the annotation exists.
*/
native private boolean isDeclaredAnnotationPresent(Class<? extends Annotation> annotationClass);
跟踪一下这个 Native 的方法源码:
https://android.googlesource.com/platform/dalvik/+/android-4.3.1_r1/vm/native/java_lang_Class.cpp
https://android.googlesource.com/platform/dalvik/+/android-4.3.1_r1/vm/reflect/Annotation.cpp
- [vm/native/java_lang_Class.cpp] Dalvik_java_lang_Class_isDeclaredAnnotationPresent()
- [vm/reflect/Annotation.cpp] dvmIsClassAnnotationPresent()
- [vm/reflect/Annotation.cpp] getAnnotationItemFromAnnotationSet()
最终实现为:
/*
* Return the annotation item of the specified type in the annotation set, or
* NULL if the set contains no annotation of that type.
*/
static const DexAnnotationItem* getAnnotationItemFromAnnotationSet(
const ClassObject* clazz, const DexAnnotationSetItem* pAnnoSet,
int visibility, const ClassObject* annotationClazz)
{
DexFile* pDexFile = clazz->pDvmDex->pDexFile;
const DexAnnotationItem* pAnnoItem;
int i;
const ClassObject* annoClass;
const u1* ptr;
u4 typeIdx;
/* we need these later; make sure they're initialized */
if (!dvmIsClassInitialized(gDvm.classOrgApacheHarmonyLangAnnotationAnnotationFactory))
dvmInitClass(gDvm.classOrgApacheHarmonyLangAnnotationAnnotationFactory);
if (!dvmIsClassInitialized(gDvm.classOrgApacheHarmonyLangAnnotationAnnotationMember))
dvmInitClass(gDvm.classOrgApacheHarmonyLangAnnotationAnnotationMember);
for (i = 0; i < (int) pAnnoSet->size; i++) {
pAnnoItem = dexGetAnnotationItem(pDexFile, pAnnoSet, i);
if (pAnnoItem->visibility != visibility)
continue;
ptr = pAnnoItem->annotation;
typeIdx = readUleb128(&ptr);
annoClass = dvmDexGetResolvedClass(clazz->pDvmDex, typeIdx);
if (annoClass == NULL) {
annoClass = dvmResolveClass(clazz, typeIdx, true);
if (annoClass == NULL) {
ALOGE("Unable to resolve %s annotation class %d",
clazz->descriptor, typeIdx);
Thread* self = dvmThreadSelf();
assert(dvmCheckException(self));
dvmClearException(self);
continue;
}
}
if (annoClass == annotationClazz) {
return pAnnoItem;
}
}
return NULL;
}
可以看到,在这个方法的第22行,也是对可见性做了判断了的,因此最后的结果没有问题。
而 Android 在 7.0 之后,不知道为什么又去掉了 5.x/6.x 中的 AnnotationAccess 类,改回了用 Native实现,最后的结果也没有问题……
其它一些版本的源码
- 4.4(API 19)的源码是 Native 实现,没有问题
- 4.4.2 版本的手机就已经有了 AnnotationAccess 这个类,并且也没有做可见行过滤,但实测结果是正确的
- 4.4.3 版本的源码,与 4.4.2 相同,不过暂时没有手机做测试
精力有限,暂时就没有一个一个版本去仔细看了
结论
因此可以得到一些结论:
- 使用 Class.getAnnotations() 方法得到的结果在所有版本都是正确的
- 使用 Class.isAnnotationPresent() 方法在 5.x /6.x 版本上对于 CLASS 级别的注解会返回错误的结果 True,其它版本返回正确的结果 False
- 使用 Class.isAnnotationPresent() 方法对于 SOURSE 和 RUNTIME 级别的注解在各版本均能返回正确的结果
基于这几个结论,我们在代码里应该尽量避免使用 Class.isAnnotationPresent() 这个方法,如果必须要使用,请确保你传入的注解类是 Runtime 级别的。
ps:理论上这是系统实现的BUG,应该很多人遇到了才对,但是不知道为什么我搜了好久都没有搜到相关的文章 Orz…