DexMetadataHelper
本文解决一个具体问题:.dm 文件从安装输入变成 dexopt 参数时,哪些检查由 framework 完成,哪些判断由 ART Service 完成,失败后又怎样降级。读者可先阅读 ART profiles 与 云端 profiles,了解 profile 的运行时来源和设备端交付语义。本文不讲 Play 服务端如何生成 profile,也不把 .dm 内部格式的每个未来扩展当作当前平台契约。
Android 17 中有两个同名类:android.content.pm.dex.DexMetadataHelper 负责安装层路径和容器合法性;com.android.server.art.DexMetadataHelper 负责 ART 层内容类型和 config.pb。名称相同,owner、输入和失败语义完全不同。
1. 两个 owner
| 层 | 类 | 输入 | 输出 | 主要调用方 |
|---|---|---|---|---|
| framework | android.content.pm.dex.DexMetadataHelper | APK/DM 路径列表 | APK 到 DM 映射、安装错误 | PackageInstallerSession、安装解析流程 |
| ART Service | com.android.server.art.DexMetadataHelper | DexMetadataPath | DexMetadataInfo | Dexopter、ArtManagerLocal |
framework 的 helper 不读取 ZIP 条目;ART 的 helper 不负责把孤立 .dm 从安装 session 中剔除。调试时先判断问题发生在哪一层,才能选择正确日志:安装失败看 INSTALL_FAILED_BAD_DEX_METADATA,dexopt 降级看 TYPE_ERROR、externalProfileErrors 和 Dexopter 日志。
2. Framework 路径
源码文件:frameworks/base/core/java/android/content/pm/dex/DexMetadataHelper.java,符号:buildDexMetadataPathForApk、buildPackageApkToDexMetadataMap
public static String buildDexMetadataPathForApk(String codePath) {
if (!ApkLiteParseUtils.isApkPath(codePath)) {
throw new IllegalStateException(
"Corrupted package. Code path is not an apk "
+ codePath);
}
return codePath.substring(0,
codePath.length() - APK_FILE_EXTENSION.length())
+ DEX_METADATA_FILE_EXTENSION;
}
public static Map<String, String> buildPackageApkToDexMetadataMap(
List<String> codePaths) {
ArrayMap<String, String> result = new ArrayMap<>();
for (int i = codePaths.size() - 1; i >= 0; i--) {
String codePath = codePaths.get(i);
String dexMetadataPath = buildDexMetadataPathForFile(
new File(codePath));
if (Files.exists(Paths.get(dexMetadataPath))) {
result.put(codePath, dexMetadataPath);
}
}
return result;
}正式 APK 路径把 .apk 替换为 .dm;内部的宽松匹配函数也允许非 APK 代码路径通过追加 .dm 配对。映射函数只返回存在的文件,因此“没有 DM”不会被当作安装错误;错误发生在代码路径本身不是合法 APK,或后续显式校验发现孤立 DM 时。
3. DM 校验
源码文件:frameworks/base/core/java/android/content/pm/dex/DexMetadataHelper.java,符号:validateDexPaths
public static void validateDexPaths(String[] paths) {
ArrayList<String> apks = new ArrayList<>();
for (String path : paths) {
if (ApkLiteParseUtils.isApkPath(path)) {
apks.add(path);
}
}
ArrayList<String> unmatchedDmFiles = new ArrayList<>();
for (String dmPath : paths) {
if (!isDexMetadataPath(dmPath)) {
continue;
}
boolean valid = false;
for (int i = apks.size() - 1; i >= 0; i--) {
if (dmPath.equals(buildDexMetadataPathForFile(
new File(apks.get(i))))) {
valid = true;
break;
}
}
if (!valid) {
unmatchedDmFiles.add(dmPath);
}
}
if (!unmatchedDmFiles.isEmpty()) {
throw new IllegalStateException(
"Unmatched .dm files: " + unmatchedDmFiles);
}
}这个 API 用于安装命令的路径级检查:每个 .dm 必须能映射到同一批输入中的 APK。它不打开 ZIP,因此只能证明“文件配对正确”,不能证明“ZIP 内容正确”。
源码文件:frameworks/base/core/java/android/content/pm/dex/DexMetadataHelper.java,符号:validateDexMetadataFile
public static ParseResult validateDexMetadataFile(
ParseInput input, String dmaPath,
String packageName, long versionCode) {
StrictJarFile jarFile = null;
try {
jarFile = new StrictJarFile(dmaPath, false, false);
return input.success(null);
} catch (IOException e) {
return input.error(INSTALL_FAILED_BAD_DEX_METADATA,
"Error opening " + dmaPath, e);
} finally {
if (jarFile != null) {
try {
jarFile.close();
} catch (IOException ignored) {
}
}
}
}StrictJarFile 的职责是确认容器可打开;成功结果并不表示其中含有 primary.prof 或 primary.vdex。因此合法但空的 ZIP 可以通过 framework 校验,ART 后续会把它识别为 TYPE_NONE。
4. Session 文件
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java,符号:resolveAndStageFileLocked
private void resolveAndStageFileLocked(
File origFile, File targetFile, String splitName,
List<String> artManagedFilePaths)
throws PackageManagerException {
stageFileLocked(origFile, targetFile);
if (VerityUtils.isFsVeritySupported()) {
maybeStageV4SignatureLocked(origFile, targetFile);
}
maybeStageArtManagedInstallFilesLocked(
origFile, targetFile, artManagedFilePaths);
maybeStageDigestsLocked(origFile, targetFile, splitName);
}.dm 被视为 ART managed install file,与 APK 的 stage、v4 签名和 checksums 处于同一安装文件生命周期。PMS 在此阶段不把 profile 解析成 Java 对象,也不决定 compiler filter。
private void maybeStageArtManagedInstallFilesLocked(
File origFile, File targetFile,
List<String> artManagedFilePaths)
throws PackageManagerException {
for (String path : ArtManagedInstallFileHelper.filterPathsForApk(
artManagedFilePaths, origFile.getPath())) {
File artManagedFile = new File(path);
if (!FileUtils.isValidExtFilename(
artManagedFile.getName())) {
throw new PackageManagerException(
INSTALL_FAILED_INVALID_APK,
"Invalid filename: " + artManagedFile);
}
File target = new File(
ArtManagedInstallFileHelper.getTargetPathForApk(
path, targetFile.getPath()));
stageFileLocked(artManagedFile, target);
}
}文件名合法性在 framework 层完成,目标路径由 ArtManagedInstallFileHelper 计算。更新 session 还会通过 inheritFileLocked 继承旧 APK 关联的 .dm,但继承只保证文件可见,不保证旧 profile 对新 dex 仍可用。
5. ART 的类型与配置
源码文件:art/libartservice/service/java/com/android/server/art/DexMetadataHelper.java,符号:DexMetadataInfo、getType
public record DexMetadataInfo(
@Nullable DexMetadataPath dmPath,
@NonNull DexMetadataConfig config,
@DexMetadata.Type int type) {}
private static int getType(ZipFile zipFile) {
var profile = zipFile.getEntry(
ArtConstants.DEX_METADATA_PROFILE_ENTRY);
var vdex = zipFile.getEntry(
ArtConstants.DEX_METADATA_VDEX_ENTRY);
if (profile != null && vdex != null) {
return DexMetadata.TYPE_PROFILE_AND_VDEX;
} else if (profile != null) {
return DexMetadata.TYPE_PROFILE;
} else if (vdex != null) {
return DexMetadata.TYPE_VDEX;
}
return DexMetadata.TYPE_NONE;
}DexMetadataInfo 同时携带三类信息:路径、配置和内容类型。类型只由 ZIP 条目存在性决定;profile 是否与当前 dex 对应、是否可读、是否值得用于编译,要等 Dexopter 调用 artd 的 profile 操作。
源码文件:art/libartservice/service/java/com/android/server/art/DexMetadataHelper.java,符号:getDexMetadataInfo
public DexMetadataInfo getDexMetadataInfo(
@Nullable DexMetadataPath dmPath) {
if (dmPath == null) {
return getDefaultDexMetadataInfo(DexMetadata.TYPE_NONE);
}
String realDmPath = getDmPath(dmPath);
try (var zipFile = mInjector.openZipFile(realDmPath)) {
ZipEntry entry = zipFile.getEntry("config.pb");
if (entry == null) {
return new DexMetadataInfo(dmPath,
DexMetadataConfig.getDefaultInstance(),
getType(zipFile));
}
try (InputStream stream = zipFile.getInputStream(entry)) {
return new DexMetadataInfo(dmPath,
DexMetadataConfig.parseFrom(stream),
getType(zipFile));
}
} catch (IOException e) {
if (e instanceof FileNotFoundException
|| e instanceof NoSuchFileException) {
return getDefaultDexMetadataInfo(DexMetadata.TYPE_NONE);
}
AsLog.e("Failed to read dm file '" + realDmPath + "'", e);
return getDefaultDexMetadataInfo(DexMetadata.TYPE_ERROR);
}
}缺失文件返回 TYPE_NONE,说明可选输入不存在;读取或解析异常返回 TYPE_ERROR,说明输入存在但不可用。config.pb 缺失使用 protobuf 默认实例,不能据此把 DM 判为错误。
6. Dexopter 消费
源码文件:art/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java,符号:getExternalProfiles
public static List<ProfilePath> getExternalProfiles(
@NonNull PrimaryDexInfo dexInfo) {
return List.of(
AidlUtils.buildProfilePathForPrebuilt(
dexInfo.dexPath()),
AidlUtils.buildProfilePathForDm(
dexInfo.dexPath()));
}对于 primary dex,ART 会把预置 profile 和 DM profile 都作为 external profile 候选。它们不是两个强制存在的文件;artd 会在初始化 reference profile 时判断实际可用性。
源码文件:art/libartservice/service/java/com/android/server/art/Dexopter.java,符号:dexopt
DexMetadataInfo dmInfo = mInjector.getDexMetadataHelper()
.getDexMetadataInfo(buildDmPath(dexInfo));
if (DexFile.isProfileGuidedCompilerFilter(compilerFilter)) {
InitProfileResult result = initReferenceProfile(
dexInfo,
dmInfo.config().getEnableEmbeddedProfile());
profile = result.profile();
externalProfileErrors = result.externalProfileErrors();
if (profile == null) {
result = getOrInitReferenceProfile(
dexInfo,
dmInfo.config().getEnableEmbeddedProfile());
profile = result.profile();
externalProfileErrors = result.externalProfileErrors();
ProfilePath mergedProfile = mergeProfiles(
dexInfo, profile);
if (mergedProfile != null) {
profile = mergedProfile;
profileMerged = true;
}
}
}消费顺序是:读取 DM 配置 → 尝试外部 profile 初始化 → 必要时读取本地 reference/cur 并 merge。enableEmbeddedProfile 会影响 embedded profile 是否作为输入;它不等价于“允许/禁止所有 DM profile”。
7. VDEX 与 profile
源码文件:art/libartservice/service/java/com/android/server/art/Dexopter.java,符号:getInputVdex
switch (getDexoptNeededResult.artifactsLocation) {
case ArtifactsLocation.DALVIK_CACHE:
return VdexPath.artifactsPath(
AidlUtils.buildArtifactsPathAsInput(
dexPath, isa, true));
case ArtifactsLocation.NEXT_TO_DEX:
return VdexPath.artifactsPath(
AidlUtils.buildArtifactsPathAsInput(
dexPath, isa, false));
case ArtifactsLocation.DM:
case ArtifactsLocation.SDM_DALVIK_CACHE:
case ArtifactsLocation.SDM_NEXT_TO_DEX:
// VDEX is inside the DM; pass the DM separately to dex2oat.
return null;
default:
throw new IllegalStateException(
"Unknown artifacts location "
+ getDexoptNeededResult.artifactsLocation);
}当 VDEX 位于 DM 时,getInputVdex 返回 null,DM 路径通过独立参数传给 artd.dexopt。这说明 primary.vdex 与 primary.prof 可以同处一个容器,却在 dexopt 中走不同输入通道:VDEX 服务验证/复用,profile 服务 profile-guided 编译决策。
8. reason 使用
源码文件:art/libartservice/service/java/com/android/server/art/Dexopter.java,符号:dexoptFile
if (target.dmPath() != null
&& ReasonMapping.REASONS_FOR_INSTALL.contains(
dexoptOptions.compilationReason)) {
// Signal that a DM was available during install.
dexoptOptions.compilationReason =
dexoptOptions.compilationReason + "-dm";
}
ArtdDexoptResult result = mInjector.getArtd().dexopt(
outputArtifacts, target.dexInfo().dexPath(), target.isa(),
target.dexInfo().classLoaderContext(), target.compilerFilter(),
profile, inputVdex, target.dmPath(), priorityClass,
dexoptOptions, artdCancellationSignal,
mParams.getLoggingFd());install-dm 表示安装 reason 下存在 DM 输入,主要服务统计和诊断;源码注释明确指出它不保证 DM 中每个条目都被 dex2oat 使用。实际结论必须结合 profile 是否非空、inputVdex 来源和 DexoptResult。
9. 失败与恢复
源码文件:art/libartservice/service/java/com/android/server/art/Dexopter.java,符号:无有效 profile、提交和清理分支
if (profile == null) {
compilerFilter = printAdjustCompilerFilterReason(
compilerFilter, "verify",
"there is no valid profile", dexInfo.dexPath());
session.setCompilerFilter(compilerFilter);
}
if (profile != null && succeeded
&& profile.getTag() == ProfilePath.tmpProfilePath) {
if (commitProfileChanges(profile.getTmpProfilePath())) {
profile = null;
}
}
finally {
if (profile != null
&& profile.getTag() == ProfilePath.tmpProfilePath) {
mInjector.getArtd().deleteProfile(profile);
}
}外部 profile 不可用时,ART 将 profile-guided filter 显式降为 verify,避免以空 profile 调用编译器;临时 merge 结果只有 dexopt 成功才提交,失败时由 finally 删除。这个恢复路径把“DM 有问题”限制为编译降级,不让一个可选优化输入破坏普通安装。
10. 反向证据
源码文件:art/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java,相关测试:testSnapshotAppProfileFromDm
when(mArtd.copyAndRewriteProfile(
deepEq(AidlUtils.buildProfilePathForDm(dexPath)),
any(), eq(dexPath)))
.thenAnswer(invocation -> {
var output = invocation.<OutputProfile>getArgument(1);
output.profilePath.tmpPath = tempPathForRef;
return TestingUtils.createCopyAndRewriteProfileSuccess();
});
verify(mArtd).mergeProfiles(
argThat(profiles -> profiles.stream().anyMatch(profile ->
profile.getTag() == ProfilePath.tmpProfilePath
&& profile.getTmpProfilePath().tmpPath
.equals(tempPathForRef))),
isNull(), any(), deepEq(List.of(dexPath)), any());测试先让 reference profile 不可用,再让 DM profile copy 成功,最后断言临时 reference 被送入 merge。它证明 DM profile 是实际的 external 输入,不只是类型枚举;不证明 framework 会验证 profile 的方法语义。
源码文件:art/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java,相关测试:testSnapshotAppProfileDisableEmbeddedProfile
var config = DexMetadataConfig.newBuilder()
.setEnableEmbeddedProfile(false).build();
when(mArtd.mergeProfiles(any(), any(), any(), any(), any()))
.thenReturn(false);
mArtManagerLocal.snapshotAppProfile(
mSnapshot, PKG_NAME_1, null /* splitName */);
verify(mArtd, never()).copyAndRewriteEmbeddedProfile(
any(), eq(dexPath));这个测试证明 config.pb 的开关会阻止 embedded profile 输入;它不能外推为“DM 中的 primary.prof 一律被禁用”,因为 embedded 与 DM external 是不同输入。
11. 阅读与排查
看到安装失败 INSTALL_FAILED_BAD_DEX_METADATA,先查 framework 的 StrictJarFile 打开和 session 路径配对;看到安装成功但 compiler filter 变成 verify,再查 ART 的 TYPE_ERROR/TYPE_NONE、externalProfileErrors 和 Dexopter 的无 profile 分支。
读者可以用以下问题验证是否真正走通了源码:
- 一个孤立
.dm为什么在 framework 校验阶段失败,而一个空 ZIP 为什么能通过这一阶段? TYPE_PROFILE_AND_VDEX为什么不意味着 profile 和 VDEX 会由同一个 Java 参数消费?- 更新 session 继承
.dm后,为什么仍可能在 ART 层重新 merge 或降级? install-dmreason 与“profile 实际被 dex2oat 使用”之间缺少哪一层证据?
