Android不同版本上注解作用域不同的问题追查

阅读时间 约 7 分钟

问题

有一个需求是要将传入的数据结构用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

这个方法的实现路径依次为:

  1. Class.isAnnotationPresent()
  2. AnnotationAccess.isAnnotationPresent()
  3. AnnotationAccess.isDeclaredAnnotationPresent()
  4. 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()方法的实现:

  1. Class.getDeclaredAnnotations()
  2. AnnotationAccess.getDeclaredAnnotations()
  3. 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

  1. [vm/native/java_lang_Class.cpp] Dalvik_java_lang_Class_isDeclaredAnnotationPresent()
  2. [vm/reflect/Annotation.cpp] dvmIsClassAnnotationPresent()
  3. [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实现,最后的结果也没有问题……

其它一些版本的源码

  1. 4.4(API 19)的源码是 Native 实现,没有问题
  2. 4.4.2 版本的手机就已经有了 AnnotationAccess 这个类,并且也没有做可见行过滤,但实测结果是正确的
  3. 4.4.3 版本的源码,与 4.4.2 相同,不过暂时没有手机做测试

精力有限,暂时就没有一个一个版本去仔细看了

结论

因此可以得到一些结论:

  1. 使用 Class.getAnnotations() 方法得到的结果在所有版本都是正确的
  2. 使用 Class.isAnnotationPresent() 方法在 5.x /6.x 版本上对于 CLASS 级别的注解会返回错误的结果 True,其它版本返回正确的结果 False
  3. 使用 Class.isAnnotationPresent() 方法对于 SOURSE 和 RUNTIME 级别的注解在各版本均能返回正确的结果

基于这几个结论,我们在代码里应该尽量避免使用 Class.isAnnotationPresent() 这个方法,如果必须要使用,请确保你传入的注解类是 Runtime 级别的。

ps:理论上这是系统实现的BUG,应该很多人遇到了才对,但是不知道为什么我搜了好久都没有搜到相关的文章 Orz…