配置 ART

本文讨论如何配置 ART 及其编译选项。本文涉及的主题包括系统映像的预编译配置、首次启动时(及 OTA 后)的 dex2oat 编译选项,以及如何在系统分区空间、数据分区空间和性能三者之间取得平衡。

请参阅 ART 与 DalvikDalvik 可执行文件格式以及 source.android.com 上的其他页面,了解如何使用 ART。请参阅在 Android Runtime (ART) 上验证应用行为,确保您的应用运行正常。

ART 的工作原理

ART 是面向 Android 5.0(Lollipop 或 L)版本及更高版本推出的新 Android 运行时。Dalvik 将不再可用。

请注意,本节仅简要介绍 ART 的配置。如需深入了解,请参阅 2014 年 Google I/O 大会上有关 Android Runtime 的演示内容。

ART 采用预先 (AOT) 编译的方法。这意味着,在安装时,dex 代码会被编译为 OAT 文件中的原生代码,并替换 Dalvik 的 odex 文件。这种做法有以下几点意义:

  • 与 Dalvik 相比,性能得到了提高。在实验室中测得的能耗也有相应的改善。
  • 没有运行时代码缓存。OAT 文件被映射到内存(因此可分页)。从 Proportional Set Size(简称 PSS,或各进程之间平均共享的内存)来看,OAT 文件占用的 RAM 内存似乎更大了。不过,我们发现,由于 OAT 文件可分页,而 Dalvik JIT 缓存不可分页,因此就实际内存压力而言,对系统的影响反而有所减轻。
  • 与 zygote 中的预加载类相似,ART 在编译时会尝试预先初始化一组类。这会创建一个“boot.art”文件,其中包含预先初始化的类和相关对象的压缩堆的映像。此文件会在 zygote 启动时映射到内存中。尽管这会占用额外的存储空间(通常为 10MB),但它可以加快 zygote 的启动,并可以创造机会,让系统在内存压力较大的情况下能够交换出某些预先加载的类。此外,这还有助于改善 ART 的低 RAM 性能,因为在 Dalvik 中,大部分此类信息都存储在线性分配空间的脏页中。
  • Dex 文件编译使用名为 dex2oat 的工具,比 dexopt 更耗时。所增加的时间各有不同,但是编译时间增加 2-3 倍的情况并不少见。例如,使用 dexopt 通常只需 1 秒就能安装的应用,如果使用 dex2oat,则可能需要 2-3 秒。
  • 如果启用全编译,则 OAT 文件比 odex 文件大。我们会在本文档的后面部分讨论降低此成本的选项。

编译选项

与 dexopt 相比,Dex 文件编译需要更多时间,特别是在首次启动(恢复出厂设置或接收 OTA 后)过程中必须编译用户的所有应用时,这一点尤为明显。为了减少所需的编译量,ART 支持对系统分区中的库和应用进行预先优化的选项。纳入预先优化的 dex 文件会占用系统映像的空间。因此,这些选项实际是以牺牲首次启动时间来换取系统映像大小。请注意,OTA 相对而言不是太频繁,并且之后的启动时间,无论是否进行预先优化,都应该是相同的。

WITH_DEXPREOPT

预先优化由构建选项 WITH_DEXPREOPT 控制。在 L 版本之前,该选项在“用户”构建中默认启用。自 L 版本起,该选项为选择启用的选项,需要在产品配置(如设备的 BoardConfig.mk 文件)中启用。

启用 WITH_DEXPREOPT 会导致对系统映像中的所有内容进行预先优化。如果这会导致系统映像过大,则可以指定其他选项来减少预先优化量。请注意,以下名称中带有“PREOPT”的所有构建选项都必须启用 WITH_DEXPREOPT 才能工作。

使用示例(在产品的 BoardConfig.mk 中):

WITH_DEXPREOPT := true

DONT_DEXPREOPT_PREBUILTS

启用 DONT_DEXPREOPT_PREBUILTS 可防止对预构建进行预先优化。这些都是在其 Android.mk 中指定了 include $(BUILD_PREBUILT) 的应用,例如 Gmail。跳过对可能通过 Google Play 进行更新的预构建应用的预先优化,可以节省 /system 空间,但是会增加首次启动的时间。

使用示例(在产品的 BoardConfig.mk 中):

WITH_DEXPREOPT := true
DONT_DEXPREOPT_PREBUILTS := true

WITH_DEXPREOPT_BOOT_IMG_ONLY

启用 WITH_DEXPREOPT_BOOT_IMG_ONLY 只会预先优化启动映像。启动映像由含有映像类的 boot.art 和含有启动相关的类路径代码的 boot.oat 组成。启用该选项可大幅节省 /system 空间,但也意味着在首次启动时会对所有应用进行优化。通常情况下,最好通过 DONT_DEXPREOPT_PREBUILTS 或 add-product-dex-preopt-module-config 选择性地停用应用预先优化功能。

使用示例(在产品的 BoardConfig.mk 中):

WITH_DEXPREOPT := true
WITH_DEXPREOPT_BOOT_IMG_ONLY := true

LOCAL_DEX_PREOPT

通过在模块定义中指定 LOCAL_DEX_PREOPT 选项,还可以基于单个应用启用或停用预先优化功能。这有助于停用对于可能会立即收到 Google Play 更新的应用的预先优化,因为更新会在已过时的系统映像中执行预先优化的代码。此外,这还有助于节省主要版本升级 OTA 的空间,因为用户的数据分区中可能已经有了较新版本的应用。

LOCAL_DEX_PREOPT 支持通过值“true”和“false”分别表示启用和停用预先优化。此外,如果预先优化不应将 classes.dex 文件从 apk 或 jar 文件中剥离,还可以指定“nostripping”。通常情况下,此文件会被剥离,因为预先优化之后便不再需要该文件;但若要使第三方 APK 签名保持有效状态,则最后一个选项必不可少。

使用示例(在应用的 Android.mk 中):

LOCAL_DEX_PREOPT := false

PRODUCT_DEX_PREOPT_*

自 L 之后的 Android 开放源代码项目 (AOSP) 版本起,我们已添加了大量标记,以进一步控制预先优化的执行方式。PRODUCT_DEX_PREOPT_BOOT_FLAGS 将选项传递给 dex2oat 以控制启动映像的编译方式。该选项可用于指定自定义映像类列表、已编译类的列表和编译器过滤器,这些内容将在下文进行介绍。同样,PRODUCT_DEX_PREOPT_DEFAULT_FLAGS 控制传递给 dex2oat 的默认标记,以编译除启动映像之外的所有文件,即 jar 和 apk 文件。

通过 PRODUCT_DEX_PREOPT_MODULE_CONFIGS,可为特定模块和产品配置传递 dex2oat 选项。这通过 $(call add-product-dex-preopt-module-config,<modules>,<option>) 在产品的 device.mk 文件中进行设置,其中 <modules> 为 jar 和 apk 文件各自的 LOCAL_MODULELOCAL_PACKAGE 名称的列表。借助此标记,可以对每个 dex 文件和特定设备的预先优化进行精细控制。此类微调可让 /system 空间最大限度地用于改进首次启动时间。

使用示例(在产品的 device.mk 中):

PRODUCT_DEX_PREOPT_DEFAULT_FLAGS := --compiler-filter=interpret-only
$(call add-product-dex-preopt-module-config,services,--compiler-filter=space)

通过在产品的 device.mk 文件中指定 $(call add-product-dex-preopt-module-config,<modules>,disable),这些标记还可用于选择性地停用特定模块或软件包的预先优化。

使用示例(产品的 device.mk 中):

$(call add-product-dex-preopt-module-config,Calculator,disable)

DEX_PREOPT 文件的首次启动安装

自 Android 7.0 起,设备可以使用两个系统分区来启用 A/B 系统更新。要想在控制系统分区大小和实现高效首次启动的同时允许使用 DEX_PREOPT,可以将预选文件安装在未使用的第二个系统分区中。这些文件会在首次启动时被复制到数据分区。

使用示例(在 device-common.mk 中):

PRODUCT_PACKAGES += \
     cppreopts.sh
PRODUCT_PROPERTY_OVERRIDES += \
     ro.cp_system_other_odex=1

在设备的 BoardConfig.mk 中:

BOARD_USES_SYSTEM_OTHER_ODEX := true

如需在系统映像中选择性地包含编译脚本和二进制文件,请参阅后台中的应用编译

预加载类列表

预加载类列表是 zygote 将在启动时初始化的一个类列表。通过该列表,每个应用无需单独运行这些类初始化程序,从而可以更快地启动并共享内存中的页面。预加载类列表文件默认位于 frameworks/base/preloaded-classes 中,其中包含一个针对典型的手机用途微调的列表。这可能不适用于其他设备(如穿戴式设备),而应进行相应的微调。做微调时应格外小心,因为添加太多的类会造成加载不使用的类而浪费内存;而添加的类太少又会导致每个应用都必须拥有自己的副本,同样会造成内存浪费。

使用示例(在产品的 device.mk 中):

PRODUCT_COPY_FILES += <filename>:system/etc/preloaded-classes

注意:必须将此行放置于沿用任何从 build/target/product/base.mk 中获得默认值的产品配置 makefile 之前。

映像类列表

映像类列表是 dex2oat 预先初始化并存储在 boot.art 文件中的类列表。通过该列表,zygote 可以在启动时从 boot.art 文件中加载这些结果,而无需在预加载期间自行运行这些类的初始化程序。其中一个重要特点是,从映像加载并在进程之间共享的页面是干净的,因此可在内存不足的情况下轻松将它们交换出去。在 L 版本中,默认情况下,映像类列表和预加载类列表使用同一个列表。自 L 之后的 AOSP 版本起,可以使用 PRODUCT_DEX_PREOPT_BOOT_FLAGS 指定自定义映像类。

使用示例(在产品的 device.mk 中):

PRODUCT_DEX_PREOPT_BOOT_FLAGS += --image-classes=<filename>

已编译类的列表

在 L 之后的 AOSP 版本中,可以指定使用已编译类的列表,在预先优化期间编译来自启动的类路径的类子集。对于空间非常紧张且无法满足整个预先优化启动映像需求的设备来说,此选项很有帮助。不过,请注意,此列表未指定的类将不会被编译(即使在设备上也不会被编译),且必须对其进行解释,这可能会影响运行时性能。默认情况下,dex2oat 会在 $OUT/system/etc/compiled-classes 中查找已编译类的列表,因此,可以通过 device.mk 将自定义的类列表复制到该位置。此外,还可以使用 PRODUCT_DEX_PREOPT_BOOT_FLAGS 指定特定文件位置。

使用示例(在产品的 device.mk 中):

PRODUCT_COPY_FILES += <filename>:system/etc/compiled-classes

注意:必须将此行放置于沿用任何从 build/target/product/base.mk 中获得默认值的产品配置 makefile 之前。

编译器过滤器

在 L 版本中,dex2oat 通过各种编译器过滤器选项来控制其编译方式。传递特定应用的编译器过滤器标记可指定其预先优化的方式。下面对各个可用选项进行了说明:

  • everything - 编译几乎所有内容,但太大以致无法通过编译器的内部表示法进行表示的类初始化程序及一些罕见的方法除外。
  • speed - 编译大多数方法并尽可能提升运行时性能,这是默认选项。
  • balanced - 尝试在编译投入上获得最佳性能回报。
  • space - 编译有限数量的方法,并优先编译存储空间相关的部分。
  • interpret-only - 跳过所有编译并依靠解释器来运行代码。
  • verify-none - 跳过验证和编译的特殊选项,应仅用于可信系统代码。

WITH_DEXPREOPT_PIC

在 Android 5.1.0 到 Android 6.0.1 的版本中,可以指定 WITH_DEXPREOPT_PIC 以启用位置无关代码 (PIC)。这样一来,就不必将来自映像的编译代码从 /system 迁移到 /data/dalvik-cache,因此可以节省数据分区中的空间。不过,因为该选项会停用利用位置相关代码进行的优化,所以会对运行时产生轻微的影响。通常情况下,需要节省 /data 空间的设备应启用 PIC 编译。

使用示例(在产品的 device.mk 中):

WITH_DEXPREOPT := true
WITH_DEXPREOPT_PIC := true

自 Android 7.0 起,PIC 编译默认处于启用状态。

WITH_ART_SMALL_MODE

对于空间非常有限的设备,可以启用 WITH_ART_SMALL_MODE。此选项仅编译启动相关的类路径,由于跳过了大多数编译,因此可以大大缩短首次启动时间。此选项还可以节省存储空间,因为没有针对应用的编译代码。但是,由于必须解释应用代码,因此这会影响运行时性能。不过,由于仍会编译框架中的大部分性能敏感型代码,因此对运行时性能的影响非常有限,但是在基准化分析中的表现可能会出现退化的情况。

使用示例(在产品的 device.mk 中):

WITH_ART_SMALL_MODE := true

在未来的版本中,该选项可以通过以下代码(在产品的 device.mk 中)来实现,因此会将其移除:

PRODUCT_PROPERTY_OVERRIDES += \
     dalvik.vm.dex2oat-filter=interpret-only \
     dalvik.vm.image-dex2oat-filter=speed

dalvik.vm 属性

ART 中的大多数 dalvik.vm 属性都与 Dalvik 类似,但是新增了以下属性。请注意,这些选项在设备编译期间和预先优化期间都会影响 dex2oat,但是前面讨论的大多数选项只会影响预先优化。

在 dex2oat 编译启动映像时对其进行控制:

  • dalvik.vm.image-dex2oat-Xms:初始堆大小
  • dalvik.vm.image-dex2oat-Xmx:最大堆大小
  • dalvik.vm.image-dex2oat-filter:编译器过滤器选项
  • dalvik.vm.image-dex2oat-threads:要使用的线程数

在 dex2oat 编译除启动映像之外的所有内容时对其进行控制:

  • dalvik.vm.dex2oat-Xms:初始堆大小
  • dalvik.vm.dex2oat-Xmx:最大堆大小
  • dalvik.vm.dex2oat-filter:编译器过滤器选项

Android 6.0 之前的版本提供了一个适用于编译除启动映像之外的所有内容的附加选项:

  • dalvik.vm.dex2oat-threads:要使用的线程数

自 Android 6.1 起,该选项变成了两个适用于编译除启动映像之外的所有内容的附加选项:

  • dalvik.vm.boot-dex2oat-threads:启动时要使用的线程数
  • dalvik.vm.dex2oat-threads:启动后要使用的线程数

Android 7.1 及之后的版本提供了两个选项来控制编译除启动映像之外的所有内容时的内存使用方式:

  • dalvik.vm.dex2oat-very-large:停用 AOT 编译的最小总 dex 文件大小(以字节为单位)
  • dalvik.vm.dex2oat-swap:使用 dex2oat 交换文件(用于低内存设备)

控制 dex2oat 的初始堆大小和最大堆大小的选项,可以限制可对哪些应用进行编译,因此不应被减少。

使用示例

这些编译器选项的目标是通过利用系统和数据分区中的可用空间,来减少必须由设备执行的 dex2oat 的数量。

如果设备具有充足的系统和数据空间,则启用 dex 预先优化十分必要。

BoardConfig.mk:

WITH_DEXPREOPT := true

如果这导致系统映像变得过大,可以尝试停用预构建的预先优化。

BoardConfig.mk:

WITH_DEXPREOPT := true
DONT_DEXPREOPT_PREBUILTS := true

如果系统映像仍然很大,则可以尝试仅对启动映像进行预先优化。

BoardConfig.mk:

WITH_DEXPREOPT := true
WITH_DEXPREOPT_BOOT_IMG_ONLY := true

不过,如果仅对启动映像进行预先优化,那么所有的应用就只能在首次启动时优化。为了避免出现这种情况,可以将这些高级标记与更精细的控件结合使用,以期预先优化尽可能多的应用。

例如,如果停用对预构建的预先优化可以达到基本适合系统分区,则通过“space”选项编译启动相关的类路径就可以达到完全适合。请注意,这会减少编译启动相关的类路径中的方法,因此有可能会解释更多代码,进而影响运行时性能。

BoardConfig.mk:

WITH_DEXPREOPT := true
DONT_DEXPREOPT_PREBUILTS := true

device.mk:

PRODUCT_DEX_PREOPT_BOOT_FLAGS := --compiler-filter=space

如果设备的系统分区空间非常有限,则可以使用已编译类列表编译启动相关的类路径中的类子集。因为必须对未包含在此列表中的启动相关的类路径方法进行解释,所以可能会影响运行时性能。

BoardConfig.mk:

WITH_DEXPREOPT := true
WITH_DEXPREOPT_BOOT_IMG_ONLY := true

device.mk:

PRODUCT_COPY_FILES += <filename>:system/etc/compiled-classes

如果设备的系统分区空间和数据分区空间都很有限,则可以使用编译器过滤器标记来停用对某些应用的编译。在这种情况下,由于不会有任何编译代码,因此会节省系统和数据分区的空间,但是必须对这些应用进行解释。此示例配置会预先优化启动相关的类路径,但会阻止编译不属于预构建的其他应用。不过,为了防止 system_server 出现明显的性能下降,仍会对 services.jar 进行编译,但对空间占用进行了优化。请注意,用户安装的应用仍将使用默认的 speed 编译器过滤器。

BoardConfig.mk:

WITH_DEXPREOPT := true
DONT_DEXPREOPT_PREBUILTS := true

device.mk:

PRODUCT_DEX_PREOPT_DEFAULT_FLAGS := --compiler-filter=interpret-only
$(call add-product-dex-preopt-module-config,services,--compiler-filter=space)

对于主要版本升级 OTA,由于某些应用可能已过期,因此将它们添加到黑名单以避免对其进行预先优化,会非常有帮助。可以通过指定 LOCAL_DEX_PREOPT(针对所有产品)或使用 PRODUCT_DEX_PREOPT_MODULE_CONFIGS(针对特定产品)来实现。

BoardConfig.mk:

WITH_DEXPREOPT := true

Android.mk(已添加到黑名单的应用):

LOCAL_DEX_PREOPT := false