Dexpreopt 和 检查

Android 12 针对具有 <uses-library> 依赖项的 Java 模块对 DEX 文件 (dexpreopt) 的 AOT 编译进行了构建系统变更。在某些情况下,这些构建系统变更可能会破坏 build。请参照本文针对破坏情况做好准备,并按照本文中的诀窍来修复和缓解这些情况。

dexpreopt 是预先编译 Java 库和应用的过程。dexpreopt 是在构建时在主机端进行的(这与在设备端发生的 dexopt 相反)。Java 模块(库或应用)所用的共享库依赖项的结构称为其“类加载器上下文”(CLC)。为了保证 dexpreopt 的正确性,构建时 CLC 和运行时 CLC 必须一致。构建时 CLC 是 dex2oat 编译器在 dexpreopt 时间(记录在 ODEX 文件中)所用的内容,而运行时 CLC 是在设备端加载预编译代码时的上下文。

出于正确性和性能方面的原因,这些构建时 CLC 必须与运行时 CLC 一致。为确保正确性,有必要处理重复的类。如果运行时的共享库依赖项与用于编译的依赖项不同,某些类的解析方式可能会有所不同,从而导致细微的运行时 bug。性能也会受到对重复类进行运行时检查的影响。

受影响的用例

首次启动是受这些变更影响的主要用例:如果 ART 检测到构建时 CLC 与运行时 CLC 之间存在不匹配情况,则会拒绝 dexpreopt 工件,并转为运行 dexopt。对于后续启动,这没有问题,因为应用可以在后台进行 dex 处理并存储在磁盘上。

受影响的 Android 区域

这会影响对其他 Java 库具有运行时依赖项的所有 Java 应用和库。Android 拥有数千款应用,其中有数百款应用使用的是共享库。合作伙伴也会受到影响,因为他们有自己的库和应用。

破坏性更改

构建系统需要知道 <uses-library> 依赖项,然后才能生成 dexpreopt 构建规则。不过,它无法直接访问清单,也无法读取其中的 <uses-library> 标记,因为构建系统在生成构建规则时不能读取任意文件(出于性能方面的原因)。此外,清单可能会打包到 APK 或预构建实现中。因此,<uses-library> 信息必须存在于构建文件(Android.bpAndroid.mk)中。

以前,ART 使用一种忽略共享库依赖项的权宜解决方法(称为 &-classpath)。这种方法不安全,会导致一些细微 bug,因此 Android 12 已移除该解决方法。

因此,未在构建文件中提供正确 <uses-library> 信息的 Java 模块可能会导致 build 破坏(由构建时 CLC 不匹配引起)或首次启动时间回归(由启动时 CLC 不匹配和随后运行的 dexopt 引起)。

迁移途径

请按照以下步骤修正遭到破坏的 build:

  1. 在全局范围内停用对某个特定产品的构建时检查,方法是设置

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    (在产品 makefile 中)。这样可以修正构建错误(修正破坏情况部分中列出的特殊情况除外)。不过,这是一种临时的权宜解决方法,可能会导致启动时 CLC 不匹配,然后运行 dexopt。

  2. 不妨在构建文件中添加必要的 <uses-library> 信息,修正失败的模块,然后再在全局范围内停用构建时检查(如需了解详情,请参阅修正破坏情况)。对于大多数模块,这需要在 Android.bpAndroid.mk 中添加几行代码。

  3. 按模块对有问题的情况停用构建时检查和 dexpreopt。停用 dexpreopt 可以避免浪费构建时间,在启动时遭拒的工件也不会占用存储空间。

  4. 通过取消设置第 1 步中设置的 PRODUCT_BROKEN_VERIFY_USES_LIBRARIES,在全局范围内重新启用构建时检查;在进行此更改后,构建应该不会失败(因为执行了第 2 步和第 3 步)。

  5. 修正您在第 3 步中停用的模块(一次一个),然后重新启用 dexpreopt 和 <uses-library> 检查。如有必要,请提交 bug。

Android 12 中会强制执行构建时 <uses-library> 检查。

解决破坏问题

下面几部分介绍如何修正特定类型的破坏情况。

构建错误:CLC 不匹配

构建系统会针对 Android.bpAndroid.mk 文件与清单中的信息进行构建时一致性检查。构建系统无法读取清单,但可以通过生成构建规则来读取清单(必要时从 APK 中提取清单),并将清单中的 <uses-library> 标记与构建文件中的 <uses-library> 信息进行比较。如果检查失败,将显示如下所示的错误:

error: mismatch in the <uses-library> tags between the build system and the manifest:
    - required libraries in build system: []
                     vs. in the manifest: [org.apache.http.legacy]
    - optional libraries in build system: []
                     vs. in the manifest: [com.x.y.z]
    - tags in the manifest (.../X_intermediates/manifest/AndroidManifest.xml):
        <uses-library android:name="com.x.y.z"/>
        <uses-library android:name="org.apache.http.legacy"/>

note: the following options are available:
    - to temporarily disable the check on command line, rebuild with RELAX_USES_LIBRARY_CHECK=true (this will set compiler filter "verify" and disable AOT-compilation in dexpreopt)
    - to temporarily disable the check for the whole product, set PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true in the product makefiles
    - to fix the check, make build system properties coherent with the manifest
    - see build/make/Changes.md for details

正如错误消息所示,存在多种解决方案,具体取决于紧急程度:

  • 如需进行临时的产品级修正,请在产品 makefile 中设置 PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true。构建时一致性检查仍会执行,但检查失败并不意味着构建失败。取而代之的是,检查失败会使构建系统将 dex2oat 编译器过滤器降级为 dexpreopt 中的 verify,从而使此模块的 AOT 编译完全停用。
  • 如需进行快速的全局命令行修正,请使用环境变量 RELAX_USES_LIBRARY_CHECK=true。它的效果与 PRODUCT_BROKEN_VERIFY_USES_LIBRARIES 相同,但旨在用于命令行。该环境变量会替换产品变量。
  • 如需获得从根源上修正该错误的解决方案,请让构建系统知悉清单中的 <uses-library> 标记。通过检查错误消息,可以看到哪些库导致了问题(正如检查 AndroidManifest.xml 或 APK 内的清单一样,可以用 `aapt dump badging $APK | grep uses-library` 来检查)。

对于 Android.bp 模块:

  1. 在模块的 libs 属性中查找缺失的库。如果库存在,Soong 通常会自动添加此类库,但以下特殊情况除外:

    • 该库不是 SDK 库(定义为 java_library,而不是 java_sdk_library)。
    • 库的库名称(在清单中)与其模块名称(在构建系统中)不同。

    如需暂时修正此问题,请在 Android.bp 库定义中添加 provides_uses_lib: "<library-name>"。如需获取长期解决方案,请修正根本问题:将该库转换为 SDK 库,或重命名其模块。

  2. 如果上一步中未提供解决方案,请向模块的 Android.bp 定义添加 uses_libs: ["<library-module-name>"](针对必需的库)或 optional_uses_libs: ["<library-module-name>"](针对可选的库)。这些属性接受一个模块名称列表。列表中库的相对顺序必须与清单中的顺序相同。

对于 Android.mk 模块:

  1. 检查库的库名称(在清单中)与其模块名称(在构建系统中)是否不同。如果不同,暂时的修正方案是在库的 Android.mk 文件中添加 LOCAL_PROVIDES_USES_LIBRARY := <library-name>,或在库的 Android.bp 文件中添加 provides_uses_lib: "<library-name>"(这两种情况都有可能出现,因为 Android.mk 模块可能依赖于 Android.bp 库)。如需获取长期解决方案,请修正根本问题:重命名库模块。

  2. 向模块的 Android.mk 定义添加 LOCAL_USES_LIBRARIES := <library-module-name>(针对必需的库)或 LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name>(针对可选的库)。这些属性接受一个模块名称列表。列表中库的相对顺序必须与清单中的顺序相同。

构建错误:未知的库路径

如果构建系统找不到指向 <uses-library> DEX jar 的路径(主机端构建时路径或设备端安装路径),则通常无法成功构建。如果找不到路径,这可能表示该库的配置方式不符合预期。对有问题的模块停用 dexpreopt 可以暂时修正 build。

Android.bp(模块属性):

enforce_uses_libs: false,
dex_preopt: {
    enabled: false,
},

Android.mk(模块变量):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

提交 bug,调查所有不受支持的场景。

构建错误:缺少库依赖项

如果尝试将模块 Y 的清单中的 <uses-library> X 添加到 Y 的构建文件中,则可能会因缺失依赖项 X 而导致构建错误。

下面是 Android.bp 模块的错误消息示例:

"Y" depends on undefined module "X"

下面是 Android.mk 模块的错误消息示例:

'.../JAVA_LIBRARIES/com.android.X_intermediates/dexpreopt.config', needed by '.../APPS/Y_intermediates/enforce_uses_libraries.status', missing and no known rule to make it

此类错误的常见原因是库的命名方式与相应模块在构建系统中的命名方式不同。例如,如果清单 <uses-library> 条目为 com.android.X,但库模块的名称只是 X,就会导致错误。为了解决此问题,请告知构建系统:名为 X 的模块提供了一个名为 com.android.X<uses-library>

以下是 Android.bp 库(模块属性)的示例:

provides_uses_lib: “com.android.X”,

以下是 Android.mk 库(模块变量)的示例:

LOCAL_PROVIDES_USES_LIBRARY := com.android.X

启动时 CLC 不匹配

首次启动时,在 logcat 中搜索与 CLC 不匹配相关的消息,如下所示:

$ adb wait-for-device && adb logcat \
  | grep -E 'ClassLoaderContext [a-z ]+ mismatch' -A1

输出内容中可能包含如下形式的消息:

[...] W system_server: ClassLoaderContext shared library size mismatch Expected=..., found=... (PCL[]... | PCL[]...)
[...] I PackageDexOptimizer: Running dexopt (dexoptNeeded=1) on: ...

如果您收到 CLC 不匹配警告,请查找故障模块的 dexopt 命令。如需解决此问题,请确保通过模块的构建时检查。如果这不起作用,您可能遇到了构建系统不支持的特殊情况(例如,应用加载了另一个 APK,而非库)。构建系统不一定能处理所有情况,因为在构建时无法确知应用在运行时加载的内容。

类加载器上下文

CLC 是一种树状结构,用于描述类加载器层次结构。构建系统使用了狭义的 CLC 概念(它只涵盖库,不涵盖 APK 或自定义类加载器):它是库树,代表一个库或应用的所有 <uses-library> 依赖项的传递闭包。CLC 的顶层元素是在清单(类路径)中指定的直接 <uses-library> 依赖项。CLC 树的每个节点都是一个 <uses-library> 节点,可能有自己的 <uses-library> 子节点。

由于 <uses-library> 依赖项是一种有向无环图,不一定是树,因此 CLC 可能包含同一库的多个子树。换句话说,CLC 是“展开”到树的依赖项图。复制操作仅在逻辑层面执行;实际的底层类加载器不会复制(在运行时,每个库都有一个类加载器实例)。

CLC 会在解析库或应用所用的 Java 类时定义库的查找顺序。查询顺序很重要,因为库可能包含重复的类,而类将解析为首个匹配项。

设备端(运行时)CLC

PackageManager(在 frameworks/base 中)会创建 CLC,在设备端加载 Java 模块。它会将模块清单的 <uses-library> 标记中列出的库添加为顶级 CLC 元素。

对于每个用过的库,PackageManager 都会获取其所有 <uses-library> 依赖项(在该库的清单中指定为标记),并为每个依赖项添加嵌套 CLC。此过程将持续递归执行,直到构造的 CLC 树的所有叶节点都变成不具有 <uses-library> 依赖项的库。

PackageManager 只能感知共享库。此使用场景中“共享”的定义不同于其常规含义(如“共享”不同于“静态”)。在 Android 中,Java 共享库列于在设备端安装的 XML 配置中 (/system/etc/permissions/platform.xml)。每个条目都包含共享库的名称、指向其 DEX jar 文件的路径,以及依赖项列表(这个库在运行时使用的其他共享库,并在其清单的 <uses-library> 标记中指定)。

换句话说,有两个信息来源允许 PackageManager 在运行时构建 CLC:清单中的 <uses-library> 标记,以及 XML 配置中的共享库依赖项。

主机端(构建时)CLC

不仅在加载库或应用时不需要 CLC,在编译库或应用时也不需要。编译要么在设备端 (dexopt) 进行,要么在构建期间 (dexpreopt) 进行。由于 dexopt 发生在设备端,因此它与 PackageManager 具有相同的信息(清单和共享库依赖项)。不过,dexpreopt 发生在主机端,在完全不同的环境中进行,并且必须从构建系统获取相同的信息。

因此,dexpreopt 所用的构建时 CLC 与 PackageManager 所用的运行时 CLC 是相同的,但计算方式不同。

构建时 CLC 必须与运行时 CLC 一致,否则系统会拒绝由 dexpreopt 创建且经过 AOT 编译的代码。如需检查构建时 CLC 和运行时 CLC 的一致性,dex2oat 编译器会将构建时 CLC 记录在 *.odex 文件中(具体为 OAT 文件标头的 classpath 字段中)。如需查找存储的 CLC,请使用以下命令:

oatdump --oat-file=<FILE> | grep '^classpath = '

在启动期间,构建时 CLC 和运行时 CLC 不匹配的情况将在 logcat 中报告。使用以下命令即可搜索:

logcat | grep -E 'ClassLoaderContext [a-z ]+ mismatch'

不匹配会降低性能,因为它会强制对库或应用进行 dex 处理,或者使其在不进行优化的情况下运行(例如,应用的代码可能需要从 APK 的内存中提取,这项操作耗费的资源非常多)。

共享库可以设置为可选或必需。从 dexpreopt 的角度来看,必需库在构建时必须存在(缺少这个必需库会导致构建错误)。在构建时,可选库可以存在,也可以不存在;如果存在,系统会将其添加到 CLC 中、传递给 dex2oat 并记录在 *.odex 文件中。如果没有可选库,系统会跳过它,并且不会将其添加到 CLC 中。如果构建时状态和运行时状态不匹配(可选库在一种情况下存在,但在另一种情况下不存在),则构建时 CLC 和运行时 CLC 会不匹配,编译的代码会被拒绝。

高级构建系统详情(清单修复程序)

有时,库或应用的源清单中会缺失 <uses-library> 标记。例如,如果库或应用的一个传递依赖项开始使用另一个 <uses-library> 标记,而该库或应用的清单未更新为包含该标记,就会发生这种情况。

Soong 可以自动计算指定库或应用的某些缺失 <uses-library> 标记,作为库或应用的传递依赖项闭包中的 SDK 库。之所以需要该闭包,是因为相应库(或应用)可能依赖于某个依赖于 SDK 库的静态库,并且可能会再次传递性地依赖于另一个库。

并非所有 <uses-library> 标记都可以通过这种方式计算,但最好让 Soong 自动添加清单条目;这样更不容易出错,并且也简化了维护流程。例如,当许多应用使用添加了新的 <uses-library> 依赖项的静态库时,必须更新所有应用,而这难以维护。