在 Android 运行时 (ART) 上验证应用行为

Android 运行时 (ART) 是运行 Android 5.0 (API 级别 21) 及更高版本的设备的默认运行时。该运行时提供了多项功能,可提高 Android 平台和应用的性能和流畅度。如需详细了解 ART 的新功能,请参阅《ART 简介》。

然而,某些适用于 Dalvik 的技术在 ART 上可能不起作用。本文档将介绍将现有应用迁移到 ART 兼容时需要注意的事项。大多数应用在 ART 上运行时应该能正常工作。

解决垃圾回收 (GC) 问题

在 Dalvik 上,应用经常发现显式调用 System.gc() 来触发垃圾回收 (GC) 非常有用。对于 ART 来说,这将大大减少必要性,尤其是当您为了防止出现 GC_FOR_ALLOC 类型事件或减少碎片而调用垃圾回收时。您可以通过调用 System.getProperty("java.vm.version") 来验证正在使用的运行时。如果正在使用 ART,则该属性的值为 "2.0.0" 或更高版本。

ART 使用并发复制 (CC) 收集器,该收集器会并发压缩 Java 堆。因此,您应避免使用与压缩式 GC 不兼容的技术(例如保存指向对象实例数据的指针)。这对于使用 Java 原生接口 (JNI) 的应用尤为重要。如需了解详情,请参阅《防止 JNI 问题》。

防止 JNI 问题

ART 的 JNI 比 Dalvik 的 JNI 更严格一些。使用 CheckJNI 模式来捕获常见问题是一个特别好的方法。如果您的应用使用 C/C++ 代码,您应该查阅以下文章

使用 CheckJNI 调试 Android JNI

检查 JNI 代码中的垃圾回收问题

并发复制 (CC) 收集器可能会在内存中移动对象以进行压缩。如果您使用 C/C++ 代码,请勿执行与压缩式 GC 不兼容的操作。我们已增强 CheckJNI,以识别一些潜在问题(如《ICS 中的 JNI 局部引用更改》中所述)。

尤其需要注意的一个方面是使用 Get...ArrayElements()Release...ArrayElements() 函数。在非压缩式 GC 的运行时中,Get...ArrayElements() 函数通常会返回对支持数组对象的实际内存的引用。如果您对返回的数组元素之一进行更改,则数组对象本身会发生更改(并且 Release...ArrayElements() 的参数通常会被忽略)。但是,如果使用压缩式 GC,则 Get...ArrayElements() 函数可能会返回内存副本。如果在使用压缩式 GC 时误用引用,可能会导致内存损坏或其他问题。例如

  • 如果您对返回的数组元素进行了任何更改,则完成操作后必须调用相应的 Release...ArrayElements() 函数,以确保您所做的更改已正确复制回基础数组对象。
  • 当您释放内存数组元素时,必须根据您所做的更改使用相应的模式
    • 如果您没有对数组元素进行任何更改,请使用 JNI_ABORT 模式,该模式会释放内存,而不会将更改复制回基础数组对象。
    • 如果您对数组进行了更改,并且不再需要该引用,请使用代码 0(它会更新数组对象并释放内存副本)。
    • 如果您对数组进行了要提交的更改,并且希望保留数组副本,请使用 JNI_COMMIT(它会更新基础数组对象并保留副本)。
  • 当您调用 Release...ArrayElements() 时,请返回与 Get...ArrayElements() 最初返回的指针相同的指针。例如,递增原始指针(以扫描返回的数组元素),然后将递增后的指针传递给 Release...ArrayElements() 是不安全的。传递此修改后的指针可能会导致错误的内存被释放,从而导致内存损坏。

错误处理

ART 的 JNI 在 Dalvik 不会抛出错误的一些情况下会抛出错误。(再次强调,您可以通过使用 CheckJNI 进行测试来捕获许多此类情况。)

例如,如果使用不存在的方法调用 RegisterNatives(可能是因为该方法已被 ProGuard 等工具移除),ART 现在会正确地抛出 NoSuchMethodError

08-12 17:09:41.082 13823 13823 E AndroidRuntime: FATAL EXCEPTION: main
08-12 17:09:41.082 13823 13823 E AndroidRuntime: java.lang.NoSuchMethodError:
    no static or non-static method
    "Lcom/foo/Bar;.native_frob(Ljava/lang/String;)I"
08-12 17:09:41.082 13823 13823 E AndroidRuntime:
    at java.lang.Runtime.nativeLoad(Native Method)
08-12 17:09:41.082 13823 13823 E AndroidRuntime:
    at java.lang.Runtime.doLoad(Runtime.java:421)
08-12 17:09:41.082 13823 13823 E AndroidRuntime:
    at java.lang.Runtime.loadLibrary(Runtime.java:362)
08-12 17:09:41.082 13823 13823 E AndroidRuntime:
    at java.lang.System.loadLibrary(System.java:526)

如果调用 RegisterNatives 时没有方法,ART 还会记录一个错误(可在 logcat 中查看)

W/art     ( 1234): JNI RegisterNativeMethods: attempt to register 0 native
methods for <classname>

此外,JNI 函数 GetFieldID()GetStaticFieldID() 现在会正确地抛出 NoSuchFieldError,而不是简单地返回 null。同样,GetMethodID()GetStaticMethodID() 现在会正确地抛出 NoSuchMethodError。这可能导致 CheckJNI 失败,因为存在未处理的异常或异常被抛给原生代码的 Java 调用方。因此,使用 CheckJNI 模式测试 ART 兼容的应用尤为重要。

ART 期望 JNI CallNonvirtual...Method() 方法(例如 CallNonvirtualVoidMethod())的用户使用方法的声明类,而不是子类,这是 JNI 规范的要求。

防止堆栈大小问题

Dalvik 为原生代码和 Java 代码分别拥有独立的堆栈,默认 Java 堆栈大小为 32KB,默认原生堆栈大小为 1MB。ART 具有统一的堆栈,以获得更好的局部性。通常,ART Thread 堆栈大小应与 Dalvik 大致相同。但是,如果您明确设置了堆栈大小,则可能需要重新检查在 ART 中运行的应用的这些值。

  • 在 Java 中,检查对 Thread 构造函数的调用,这些调用指定了显式堆栈大小。例如,如果发生 StackOverflowError,则需要增加大小。
  • 在 C/C++ 中,检查同时通过 JNI 运行 Java 代码的线程对 pthread_attr_setstack()pthread_attr_setstacksize() 的使用情况。以下是当应用尝试在 pthread 大小过小时调用 JNI AttachCurrentThread() 时记录的错误示例
    F/art: art/runtime/thread.cc:435]
        Attempt to attach a thread with a too-small stack (16384 bytes)

对象模型更改

Dalvik 错误地允许子类重写包私有方法。在这种情况下,ART 会发出警告

Before Android 4.1, method void com.foo.Bar.quux()
would have incorrectly overridden the package-private method in
com.quux.Quux

如果您打算重写不同包中类的某个方法,请将该方法声明为 publicprotected

Object 现在拥有私有字段。反射其类层次结构中字段的应用应注意不要尝试查看 Object 的字段。例如,如果您作为序列化框架的一部分迭代类层次结构,请在以下情况下停止

Class.getSuperclass() == java.lang.Object.class

而不是继续直到方法返回 null

如果不存在参数,Proxy InvocationHandler.invoke() 现在会收到 null,而不是空数组。此行为之前已记录,但在 Dalvik 中未正确处理。早期版本的 Mockito 在此方面存在问题,因此在使用 ART 进行测试时请使用更新的 Mockito 版本。

修复 AOT 编译问题

ART 的预编译 (AOT) Java 编译应适用于所有标准 Java 代码。编译由 ART 的 dex2oat 工具执行;如果您在安装时遇到与 dex2oat 相关的任何问题,请告知我们(请参阅《报告问题》),以便我们尽快修复。需要注意的几个问题

  • ART 在安装时执行的字节码验证比 Dalvik 更严格。Android 构建工具生成的代码应该没问题。但是,某些后处理工具(尤其是执行混淆的工具)可能会生成无效文件,这些文件在 Dalvik 中可以容忍,但在 ART 中会被拒绝。我们一直在与工具供应商合作,以发现并解决此类问题。在许多情况下,获取最新版本的工具并重新生成 DEX 文件可以解决这些问题。
  • ART 验证器标记的一些典型问题包括
    • 无效的控制流
    • 不平衡的 monitorenter/monitorexit
    • 0 长度参数类型列表大小
  • 某些应用依赖于 /system/framework/data/dalvik-cacheDexClassLoader 的优化输出目录中的已安装 .odex 文件格式。这些文件现在是 ELF 文件,而不是 DEX 文件的扩展形式。虽然 ART 试图遵循与 Dalvik 相同的命名和锁定规则,但应用不应依赖文件格式;该格式可能会在不通知的情况下更改。

    注意:在 Android 8.0 (API 级别 26) 及更高版本中,DexClassLoader 优化输出目录已弃用。如需了解详情,请参阅 DexClassLoader() 构造函数的文档。

报告问题

如果您遇到任何不是由应用 JNI 问题引起的问题,请通过 Android 开源项目问题跟踪器 https://code.google.com/p/android/issues/list 报告。如果可能,请附上 "adb bugreport" 和 Google Play 商店中应用的链接。否则,如果可能,请附上一个可重现该问题的 APK。请注意,问题(包括附件)是公开可见的。