Android 12 针对具有 <uses-library>
依赖项的 Java 模块,对 DEX 文件的 AOT 编译 (dexpreopt) 进行了构建系统更改。在某些情况下,这些构建系统更改可能会导致构建中断。请使用此页面为中断做好准备,并按照此页面上的方法修复和缓解这些中断。
Dexpreopt 是对 Java 库和应用进行提前编译的过程。Dexpreopt 在构建时在主机上发生(而不是在设备上发生的 dexopt)。Java 模块(库或应用)使用的共享库依赖项的结构称为其类加载器上下文 (CLC)。为了保证 dexpreopt 的正确性,构建时和运行时 CLC 必须一致。构建时 CLC 是 dex2oat 编译器在 dexpreopt 时使用的 CLC(记录在 ODEX 文件中),运行时 CLC 是预编译代码在设备上加载时所处的上下文。
出于正确性和性能方面的考虑,这些构建时和运行时 CLC 必须一致。为了保证正确性,有必要处理重复的类。如果运行时的共享库依赖项与编译时使用的依赖项不同,则某些类可能会以不同的方式解析,从而导致细微的运行时错误。性能也会受到重复类的运行时检查的影响。
受影响的用例
首次启动是受这些更改影响的主要用例:如果 ART 检测到构建时和运行时 CLC 之间存在不匹配,它会拒绝 dexpreopt 工件并改为运行 dexopt。对于后续启动,这没有问题,因为可以在后台 dexopt 应用并将其存储在磁盘上。
Android 受影响的区域
这会影响所有具有运行时依赖于其他 Java 库的 Java 应用和库。Android 有数千个应用,其中数百个应用使用共享库。合作伙伴也会受到影响,因为他们有自己的库和应用。
重大更改
构建系统需要在生成 dexpreopt 构建规则之前知道 <uses-library>
依赖项。但是,它无法直接访问清单并读取其中的 <uses-library>
标记,因为构建系统在生成构建规则时不允许读取任意文件(出于性能原因)。此外,清单可能打包在 APK 或预构建文件中。因此,<uses-library>
信息必须存在于构建文件(Android.bp
或 Android.mk
)中。
以前,ART 使用一种解决方法,该方法忽略共享库依赖项(称为 &-classpath
)。这不安全并且导致细微的错误,因此该解决方法已在 Android 12 中移除。
因此,未在其构建文件中提供正确 <uses-library>
信息的 Java 模块可能会导致构建中断(由构建时 CLC 不匹配引起)或首次启动时间衰退(由启动时 CLC 不匹配引起,然后进行 dexopt)。
迁移路径
请按照以下步骤修复损坏的构建
通过设置以下内容,全局禁用特定产品的构建时检查
PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true
在产品 Makefile 中。这可以修复构建错误(“修复中断”部分中列出的特殊情况除外)。但是,这是一种临时解决方法,可能会导致启动时 CLC 不匹配,然后进行 dexopt。
通过将必要的
<uses-library>
信息添加到其构建文件(有关详细信息,请参阅“修复中断”),修复在全球禁用构建时检查之前失败的模块。对于大多数模块,这需要在Android.bp
或Android.mk
中添加几行代码。在每个模块的基础上,针对有问题的案例禁用构建时检查和 dexpreopt。禁用 dexpreopt,这样您就不会在启动时被拒绝的工件上浪费构建时间和存储空间。
通过取消设置在步骤 1 中设置的
PRODUCT_BROKEN_VERIFY_USES_LIBRARIES
,全局重新启用构建时检查;在此更改之后,构建不应失败(由于步骤 2 和 3)。一次修复在步骤 3 中禁用的模块之一,然后重新启用 dexpreopt 和
<uses-library>
检查。如有必要,请提交错误。
构建时 <uses-library>
检查在 Android 12 中强制执行。
修复中断
以下部分介绍如何修复特定类型的中断。
构建错误:CLC 不匹配
构建系统在 Android.bp
或 Android.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
模块
在模块的
libs
属性中查找缺少的库。如果它在那里,则 Soong 通常会自动添加此类库,但在以下特殊情况下除外- 该库不是 SDK 库(它被定义为
java_library
而不是java_sdk_library
)。 - 该库的库名称(在清单中)与其模块名称(在构建系统中)不同。
要临时修复此问题,请在
Android.bp
库定义中添加provides_uses_lib: "<library-name>"
。对于长期解决方案,请修复根本问题:将库转换为 SDK 库,或重命名其模块。- 该库不是 SDK 库(它被定义为
如果上一步未提供解决方案,请为必需的库添加
uses_libs: ["<library-module-name>"]
,或为可选库添加optional_uses_libs: ["<library-module-name>"]
到模块的Android.bp
定义中。这些属性接受模块名称列表。列表上库的相对顺序必须与清单中的顺序相同。
对于 Android.mk
模块
检查库的库名称(在清单中)是否与其模块名称(在构建系统中)不同。如果不同,请通过在库的
Android.mk
文件中添加LOCAL_PROVIDES_USES_LIBRARY := <library-name>
,或在库的Android.bp
文件中添加provides_uses_lib: "<library-name>"
来临时修复此问题(两种情况都有可能,因为Android.mk
模块可能依赖于Android.bp
库)。对于长期解决方案,请修复根本问题:重命名库模块。为必需的库添加
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 [a-z ]+ mismatch'
不匹配会对性能产生不利影响,因为它会强制库或应用进行 dexopt,或者在没有优化的情况下运行(例如,应用的代码可能需要从 APK 中在内存中提取,这是一种非常耗时的操作)。
共享库可以是可选的,也可以是必需的。从 dexpreopt 的角度来看,必需的库必须在构建时存在(其缺失是构建错误)。可选库可以在构建时存在或不存在:如果存在,则将其添加到 CLC,传递给 dex2oat,并记录在 *.odex
文件中。如果可选库不存在,则会跳过它,并且不会将其添加到 CLC。如果构建时和运行时状态之间存在不匹配(可选库在一种情况下存在,但在另一种情况下不存在),则构建时和运行时 CLC 不匹配,并且编译的代码将被拒绝。
高级构建系统详细信息(清单修复程序)
有时,库或应用的源清单中缺少 <uses-library>
标记。例如,如果库或应用的传递依赖项之一开始使用另一个 <uses-library>
标记,并且库或应用的清单未更新以包含它,则可能会发生这种情况。
Soong 可以自动计算给定库或应用的一些缺失 <uses-library>
标记,作为库或应用的传递依赖项闭包中的 SDK 库。需要闭包是因为库(或应用)可能依赖于依赖于 SDK 库的静态库,并且可能再次通过另一个库进行传递依赖。
并非所有 <uses-library>
标记都可以通过这种方式计算,但在可能的情况下,最好让 Soong 自动添加清单条目;它不易出错并且简化了维护。例如,当许多应用使用添加新的 <uses-library>
依赖项的静态库时,所有应用都必须更新,这很难维护。