为什么在Android下使用注释会导致性能问题(变慢)?

50
我是ORMLite的首席作者,该库使用Java注释在类上构建数据库模式。我们的一个大型创业公司问题是,在Android 1.6下调用注释方法会导致性能问题,并且在3.0版本中仍然存在相同的情况。
我们发现以下简单的注释代码非常耗费GC资源,是真正的性能问题。在快速的Android设备上,1000次注释方法调用几乎需要一秒钟的时间。在我的MacBook Pro上,同样的代码可以在相同时间内完成2800万次(sic)调用。我们有一个包含25个方法的注释,我们希望每秒钟执行超过50个操作。
是否有人知道为什么会出现这种情况,是否有任何解决方法?当然,ORMLite可以在缓存此信息方面做一些事情,但是否有任何方法可以“修复”Android下的注释?谢谢。
public void testAndroidAnnotations() throws Exception {
    Field field = Foo.class.getDeclaredField("field");
    MyAnnotation myAnnotation = field.getAnnotation(MyAnnotation.class);
    long before = System.currentTimeMillis();
    for (int i = 0; i < 1000; i++)
        myAnnotation.foo();
    Log.i("test", "in " + (System.currentTimeMillis() - before) + "ms");
}
@Target(FIELD) @Retention(RUNTIME)
private static @interface MyAnnotation {
    String foo();
}
private static class Foo {
    @MyAnnotation(foo = "bar")
    String field;
}
这将导致以下日志输出:
I/TestRunner(  895): started: testAndroidAnnotations
D/dalvikvm(  895): GC freed 6567 objects / 476320 bytes in 85ms
D/dalvikvm(  895): GC freed 8951 objects / 599944 bytes in 71ms
D/dalvikvm(  895): GC freed 7721 objects / 524576 bytes in 68ms
D/dalvikvm(  895): GC freed 7709 objects / 523448 bytes in 73ms
I/test    (  895): in 854ms

编辑:

在@candrews指出正确方向后,我查看了一些代码。性能问题似乎是由Method.equals()中一些可怕的、丑陋的代码引起的。它调用了两种方法的toString(),然后进行比较。每个toString()都使用一个没有良好初始化大小的StringBuilder和大量的append方法。通过比较字段执行.equals会更快。

编辑:

有人给我提供了一个有趣的反射性能改进。我们现在使用反射来窥视AnnotationFactory类,直接读取字段列表。这使得反射类对我们来说快了20倍,因为它绕过了使用method.equals()的调用。这不是通用的解决方案,但以下是来自ORMLite SVN repository的Java代码。对于通用解决方案,请参见下面yanchenko的答案


如果您将foo属性更改为int而不是String,是否会出现类似的时间?也许这是字符串池的问题? - nicholas.hauschild
同时使用int或任何其他类型。这是关于注释而不是它们所注释的内容。谢谢。 - Gray
@Gray 根据candrews引用的问题,以及该答案评论中引用的问题,这个问题已经被解决了。你知道在哪个版本的Android上可以放心地删除ORMLite配置文件吗? - theblang
4个回答

22

感谢@candrews。我不确定你列出的问题是否完全有错,但肯定很接近。看起来Method.equals()是真正的罪魁祸首。我已经在错误上发表了评论。 - Gray
刚刚得到确认,问题确实存在。再次感谢@candrews。 - Gray
4
我认为仍然存在重大问题。我刚刚提交了 http://code.google.com/p/android/issues/detail?id=43827 。 - Jonathan Perlow

6

这是Gray和user931366想法的通用版本:

public class AnnotationElementsReader {

    private static Field elementsField;
    private static Field nameField;
    private static Method validateValueMethod;

    public static HashMap<String, Object> getElements(Annotation annotation)
            throws Exception {
        HashMap<String, Object> map = new HashMap<String, Object>();
        InvocationHandler handler = Proxy.getInvocationHandler(annotation);
        if (elementsField == null) {
            elementsField = handler.getClass().getDeclaredField("elements");
            elementsField.setAccessible(true);
        }
        Object[] annotationMembers = (Object[]) elementsField.get(handler);
        for (Object annotationMember : annotationMembers) {
            if (nameField == null) {
                Class<?> cl = annotationMember.getClass();
                nameField = cl.getDeclaredField("name");
                nameField.setAccessible(true);
                validateValueMethod = cl.getDeclaredMethod("validateValue");
                validateValueMethod.setAccessible(true);
            }
            String name = (String) nameField.get(annotationMember);
            Object val = validateValueMethod.invoke(annotationMember);
            map.put(name, val);
        }
        return map;
    }

}

我已经对一个包含4个元素的注释进行了基准测试。
在获取它们所有值或调用上述方法的10000次迭代中,毫秒时间如下:

     Device        Default  Hack
HTC Desire 2.3.7    11094   730
Emulator 4.0.4      3157    528
Galaxy Nexus 4.3    1248    392

下面是我如何将其集成到DroidParts中的方法: https://github.com/yanchenko/droidparts/commit/93fd1a1d6c76c2f4abf185f92c5c59e285f8bc69


2
+1 好的通用解决方案。我有点惊讶你在代码中没有给我(或ORMLite)解决方案的功劳。如果是我,我会这样做的。 - Gray
这个让我省了无数的时间。这个帖子里的每个人都很棒! - spy

5
为了跟进这个问题,在调用注释方法时仍存在问题。candrews列出的错误修复了getAnnotation()缓慢的问题,但由于Method.equals()问题,在注释上调用方法仍然是一个问题。
找不到Method.equals()的错误报告,因此我在此处创建一个: https://code.google.com/p/android/issues/detail?id=37380 编辑: 所以我对此进行的解决方法(感谢@Gray的想法)实际上非常简单。 (代码已被省略,其中一些缓存等也被省略)
annotationFactory = Class.forName("org.apache.harmony.lang.annotation.AnnotationFactory");
getElementDesc = annotationFactory.getMethod("getElementsDescription", Class.class);
Object[] members = (Object[])getElementDesc.invoke(annotationFactory, clz); // these are AnnotationMember[]

Object element = null;
for (Object e:members){ // AnnotationMembers
    Field f = e.getClass().getDeclaredField("name");
    f.setAccessible(true);
    String fname = (String) f.get(e);
    if (methodName.equals(fname)){
        element = e;
    break;
    }
}

if (element == null) throw new Exception("Element was not found");
Method m = element.getClass().getMethod("validateValue");
return m.invoke(element, args);

你的效率会根据使用情况而异,但在我的情况下,这种方法比“正确的方法”快了15-20倍。

哇,真的吗!我特别指出了在其他错误报告中Method.equals()有多么糟糕。 - Gray
是的,这仍然是一个问题。所以,我想我很聪明,只是打算直接在我的注释上调用Method.invoke,因为我看到那里的代码直接进入了本地方法。结果发现,本地方法最终调用AnnotationFactory.invoke(),这又导致了另一个Method.equals()调用。目前想不出其他解决办法。 - user931366
在ORMLite中,我使用反射来深入Android类。这使得事情快了10倍,但需要手动调整代码:https://ormlite.svn.sourceforge.net/svnroot/ormlite/ormlite-android/trunk/src/main/java/com/j256/ormlite/android/DatabaseTableConfigUtil.java - Gray
谢谢你的提示,Gray。我已经更新了上面的解决方案,它对我非常有效。我没有看到你的解决方案调用“validateValue”,所以我猜你的解决方案只是用于检查注释的存在并从注释成员中检索实际值? - user931366
我的解决方案是针对我的注释的。我完全绕过了Android的注释。我还没有进行性能测试,但应该会快得多。 - Gray

1

我认为如果您成功更改运行时保留策略,它就不应该那么慢。

编辑:我知道,对于您的项目来说可能不是一个选项。也许问题更多地在于您如何使用该注释,而不是一般性能不佳。


1
是的,需要使用RUNTIME保留策略。这绝对是一个查找注解方法的问题,而不是我正在使用它的问题。 - Gray

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接