Dexpreopt 和 <uses-library> 检查

Android 12 对具有<uses-library>依赖项的 Java 模块的 DEX 文件 (dexpreopt) 的 AOT 编译进行了构建系统更改。在某些情况下,这些构建系统更改可能会破坏构建。使用此页面准备破损,并按照此页面上的方法修复和减轻破损。

Dexpreopt 是 Java 库和应用程序的提前编译过程。 Dexpreopt 在构建时发生在主机上(与dexopt不同,它发生在设备上)。 Java 模块(库或应用程序)使用的共享库依赖项的结构称为其类加载器上下文(CLC)。为了保证 dexpreopt 的正确性,构建时和运行时的 CLC 必须一致。构建时 CLC 是 dex2oat 编译器在 dexpreopt 时使用的(它记录在 ODEX 文件中),运行时 CLC 是在设备上加载预编译代码的上下文。

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

受影响的用例

第一次启动是受这些更改影响的主要用例:如果 ART 检测到构建时和运行时 CLC 之间的不匹配,它会拒绝 dexpreopt 工件并改为运行 dexopt。对于后续启动,这很好,因为应用程序可以在后台进行 dexopted 并存储在磁盘上。

Android的受影响区域

这会影响所有在运行时依赖于其他 Java 库的 Java 应用程序和库。 Android 有数以千计的应用程序,其中数百个使用共享库。合作伙伴也受到影响,因为他们拥有自己的库和应用程序。

重大变化

构建系统在生成 dexpreopt 构建规则之前需要知道<uses-library>依赖关系。但是,它不能直接访问清单并读取其中的<uses-library>标签,因为构建系统在生成构建规则时不允许读取任意文件(出于性能原因)。此外,清单可能被打包在 APK 或预构建的内部。因此, <uses-library>信息必须存在于构建文件( Android.bpAndroid.mk )中。

以前 ART 使用了一种忽略共享库依赖项的解决方法(称为&-classpath )。这是不安全的,并且会导致一些细微的错误,因此在 Android 12 中删除了该解决方法。

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

迁移路径

请按照以下步骤修复损坏的构建:

  1. 通过设置全局禁用特定产品的构建时检查

    PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true

    在产品生成文件中。这修复了构建错误(除了特殊情况,在修复破损部分中列出)。但是,这是一种临时解决方法,它可能会导致引导时 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>检查。如有必要,提交错误。

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

修复破损

以下部分将告诉您如何修复特定类型的破损。

构建错误:CLC 不匹配

构建系统在Android.bpAndroid.mk文件中的信息与清单之间进行构建时一致性检查。构建系统无法读取清单,但它可以生成构建规则来读取清单(必要时从 APK 中提取),并将清单中的 <uses <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. 如果上一步没有提供解决方案,请将uses_libs: ["<library-module-name>"]添加到 Android 中,或者将可选库optional_uses_libs: ["<library-module-name>"]添加到Android.bp定义模块。这些属性接受模块名称列表。列表中库的相对顺序必须与清单中的顺序相同。

对于Android.mk模块:

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

  2. 为所需的库添加LOCAL_USES_LIBRARIES := <library-module-name> ;将可选库LOCAL_OPTIONAL_USES_LIBRARIES := <library-module-name>添加到模块的Android.mk定义中。这些属性接受模块名称列表。列表中库的相对顺序必须与清单中的相同。

构建错误:未知的库路径

如果构建系统找不到<uses-library> DEX jar 的路径(主机上的构建时路径或设备上的安装路径),通常会导致构建失败。找不到路径可能表明以某种意外方式配置了库。通过为有问题的模块禁用 dexpreopt 来临时修复构建。

Android.bp(模块属性):

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

Android.mk(模块变量):

LOCAL_ENFORCE_USES_LIBRARIES := false
LOCAL_DEX_PREOPT := false

提交错误以调查任何不受支持的方案。

构建错误:缺少库依赖项

尝试将模块 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必须一致,否则由 dexpreopt 创建的 AOT 编译代码会被拒绝。为了检查构建时和运行时 CLC 的相等性,dex2oat 编译器将构建时 CLC 记录在*.odex文件中(在 OAT 文件头的classpath字段中)。要查找存储的 CLC,请使用以下命令:

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

引导期间在 logcat 中报告构建时和运行时 CLC 不匹配。使用以下命令搜索它:

logcat | grep -E 'ClassLoaderContext [az ]+ mismatch'

不匹配对性能不利,因为它迫使库或应用程序要么被删除,要么在没有优化的情况下运行(例如,应用程序的代码可能需要从 APK 中提取到内存中,这是一个非常昂贵的操作)。

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

高级构建系统详细信息(清单修复程序)

有时,库或应用程序的源清单中缺少<uses-library>标记。例如,如果库或应用程序的传递依赖项之一开始使用另一个<uses-library>标记,并且库或应用程序的清单未更新以包含它,则可能会发生这种情况。

Soong 可以自动计算给定库或应用程序缺少的一些<uses-library>标记,作为库或应用程序的传递依赖闭包中的 SDK 库。需要关闭是因为库(或应用程序)可能依赖于依赖于 SDK 库的静态库,并且可能再次通过另一个库传递地依赖。

不是所有<uses-library>标签都可以这样计算,但是如果可能的话,最好让 Soong 自动添加清单条目;它不易出错并简化了维护。例如,当很多应用使用添加了新的<uses-library>依赖的静态库时,所有应用都必须更新,这很难维护。