在 Android Runtime (ART) 中验证应用行为

Android Runtime (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。由于未处理的异常或抛出给原生代码的 Java 调用方的异常,这可能会导致 CheckJNI 失败。因此,使用 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

如果没有参数,代理 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。请注意,这些问题(包括附件)都是公开可见的。