云端 profiles
本文承接 ART profiles 对运行时 cur/ref profile 的介绍,并聚焦一个更具体的问题:设备拿到 APK 旁边的 .dm 文件后,外部 profile 怎样被 ART Service 识别、初始化为 reference profile,并最终传给 dexopt。本文不讨论云端服务如何聚合用户数据,也不把 Play 的分发协议当作 Android 平台源码事实;设备端能证明的是“外部 profile 已经作为 ART managed install file 到达安装目录后发生什么”。
先区分三个名字:baseline profile 是 profile 内容的来源或生成方式,cloud profile 是分发链路中的产品语义,.dm 是设备端实际读取的容器。PMS 只负责验证、暂存和继承 .dm;profile 格式、config.pb、merge 及 artd 调用由 ART Service 拥有。
1. 设备端边界
设备端主链可以压缩为:
APK + matching .dm
-> PackageInstallerSession 暂存/继承
-> installed code path 旁的 .dm
-> PrimaryDexopter 读取 DexMetadataInfo
-> initReferenceProfile
-> dex2oat 接收 profile / DM
-> DexoptResult.dm 是否真的带有 profile,不能由文件扩展名推断。ART 会打开 ZIP,查看 primary.prof 和 primary.vdex 条目;没有条目时类型是 TYPE_NONE,损坏或读取异常时是 TYPE_ERROR。因此“下载了 DM”与“本次编译使用了 profile”是两个不同命题。
2. 安装文件的配对
源码文件:frameworks/base/core/java/android/content/pm/dex/DexMetadataHelper.java,符号:buildPackageApkToDexMetadataMap、validateDexMetadataFile
public static Map<String, String> buildPackageApkToDexMetadataMap(
List<String> codePaths) {
Map<String, String> result = new ArrayMap<>();
for (String codePath : codePaths) {
String dexMetadataPath = buildDexMetadataPathForFile(
new File(codePath));
if (new File(dexMetadataPath).exists()) {
result.put(codePath, dexMetadataPath);
}
}
return result;
}
public static ParseResult<StrictJarFile> validateDexMetadataFile(
ParseInput input, String dmaPath, String packageName) {
try {
StrictJarFile jarFile = new StrictJarFile(
dmaPath, false, false);
return input.success(jarFile);
} catch (IOException e) {
return input.error(INSTALL_FAILED_BAD_DEX_METADATA,
"Error opening " + dmaPath, e);
}
}framework 层只做两件事:按 APK 代码路径推导匹配的 .dm 路径,以及把无法打开的容器变成安装错误。它不解析 primary.prof,也不判断 profile 方法是否适合当前 dex;这些判断必须留在 ART 层。
源码文件:frameworks/base/core/java/android/content/pm/dex/DexMetadataHelper.java,符号:validateDexPaths
if (isDexMetadataPath(dmPath)) {
boolean matched = false;
for (String apkPath : apks) {
if (dmPath.equals(buildDexMetadataPathForFile(
new File(apkPath)))) {
matched = true;
break;
}
}
if (!matched) {
unmatchedDmFiles.add(dmPath);
}
}
if (!unmatchedDmFiles.isEmpty()) {
throw new IllegalStateException(
"Unmatched .dm files: " + unmatchedDmFiles);
}.dm 不能作为脱离 APK 的独立安装输入。配对校验先建立“哪个 DM 属于哪个 APK”的不变量,后续 ART 才能把 profile 中的 dex 文件索引解释为当前代码文件。
这张时序图把安装层与编译层分开:PMS 只把文件放到稳定位置,ART 直到 dexopt 时才读取容器内容。两层之间没有“PMS 解析 profile 后传 Java 对象”的中间状态。
3. Session 与 DM
源码文件: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);
}
// ART managed install files include dex metadata (.dm).
maybeStageArtManagedInstallFilesLocked(
origFile, targetFile, artManagedFilePaths);
maybeStageDigestsLocked(origFile, targetFile, splitName);
}安装 session 把 .dm 当作 ART managed install file,与 APK、v4 签名和 checksums 一起进入 stage 目录。这里不读取 profile 内容;它只维护文件名、目标路径和签名/摘要的对应关系。
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java,符号:maybeStageArtManagedInstallFilesLocked
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 targetArtManagedFile = new File(
ArtManagedInstallFileHelper.getTargetPathForApk(
path, targetFile.getPath()));
stageFileLocked(artManagedFile, targetArtManagedFile);
}如果 APK 在更新 session 中被继承,.dm 也会一起继承,而不是只继承 APK 本身。这样 split 替换、base APK 继承和 ART managed 文件始终保持一一对应。
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java,符号:inheritFileLocked
private void inheritFileLocked(
File origFile, List<String> artManagedFilePaths) {
mResolvedInheritedFiles.add(origFile);
maybeInheritV4SignatureLocked(origFile);
for (String path : ArtManagedInstallFileHelper.filterPathsForApk(
artManagedFilePaths, origFile.getPath())) {
mResolvedInheritedFiles.add(new File(path));
}
}更新时继承旧 .dm 并不意味着旧 profile 必然可用。ART 会在下一次 dexopt 中根据新 dex 路径、版本和 profile 内容判断是否能使用;不可用的 artifacts 或 profile 会被替换或清理。
4. ART 识别 DM
源码文件: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);
}
}缺少 .dm 是正常的 TYPE_NONE;存在但无法读取则是 TYPE_ERROR。config.pb 缺失时使用默认配置,但仍会继续判断 ZIP 中是否有 profile/vdex 条目。配置和内容类型是两个正交维度。
源码文件:art/libartservice/service/java/com/android/server/art/DexMetadataHelper.java,符号:getType
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;
}TYPE_PROFILE 只说明容器含有 profile,不代表 profile 会在本次编译中被采用;后续还要通过 dex 文件匹配、profile merge、compiler filter 和权限条件。TYPE_VDEX 也不能被误写成 profile 优化。
5. External profile
源码文件: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()));
}ART 同时考虑预置 profile 和 DM profile。两者都只是候选输入,是否存在、是否可读及是否与 dex 匹配由 artd 的 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;
}
}
}共享应用先调用 initReferenceProfile,优先尝试把外部 profile 初始化为可供其他读取者使用的 reference profile;如果没有得到 profile,再读取已有 reference 与当前用户 profile 并执行 merge。这里的 fallback 使“DM 无效”不会自动导致安装失败,而是允许退回本地 profile 或无 profile 编译。
6. 首次 dexopt
源码文件: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);
}
boolean isProfileGuidedCompilerFilter =
DexFile.isProfileGuidedCompilerFilter(compilerFilter);
Utils.check(isProfileGuidedCompilerFilter == (profile != null));这段断言是云端 profile 路径最重要的边界:profile-guided filter 与有效 profile 必须成对出现。DM 有 profile 条目但初始化失败时,ART 将 filter 调整为 verify,而不是继续以空 profile 调用 dex2oat。
源码文件:art/libartservice/service/java/com/android/server/art/Dexopter.java,符号:dexoptFile
if (target.dmPath() != null
&& ReasonMapping.REASONS_FOR_INSTALL.contains(
dexoptOptions.compilationReason)) {
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());-dm 是安装统计 reason 的信号,不是“DM 中的 profile 一定被使用”的证明。源码注释明确说明:即使 reason 带 -dm,dex2oat 仍可能忽略 DM 的部分内容,甚至传入空 DM。排查首次安装优化时必须同时查看 actual compiler filter、profile 初始化错误和 dexopt result。
图中 TYPE_PROFILE 只是进入初始化分支的条件,最终是否走绿色路径还取决于 profile 与 dex 的匹配、读取权限和 merge 结果。
7. 提交与回收
源码文件:art/libartservice/service/java/com/android/server/art/Dexopter.java,符号:commitProfileChanges 与 finally
if (profile != null && succeeded) {
if (profile.getTag() == ProfilePath.tmpProfilePath) {
if (commitProfileChanges(profile.getTmpProfilePath())) {
profile = null;
}
}
}
finally {
if (profile != null
&& profile.getTag() == ProfilePath.tmpProfilePath) {
mInjector.getArtd().deleteProfile(profile);
}
}临时 profile 只有在 dexopt 成功后才提交;失败或异常退出时,finally 删除临时文件。这样一次损坏的外部 profile 不会半提交为新的 reference profile。externalProfileErrors 会进入结果记录和日志,但普通 profile 错误通常表现为降级,而不是阻断 APK 安装。
8. Profile merge
源码文件:art/libartservice/service/java/com/android/server/art/Dexopter.java,符号:mergeProfiles、cleanupCurProfiles
private ProfilePath mergeProfiles(
DexInfoType dexInfo, ProfilePath referenceProfile)
throws RemoteException {
OutputProfile output = buildOutputProfile(
dexInfo, false /* isPublic */);
var options = new MergeProfileOptions();
options.forceMerge = (mParams.getFlags()
& ArtFlags.FLAG_FORCE_MERGE_PROFILE) != 0;
if (mInjector.getArtd().mergeProfiles(
getCurProfiles(dexInfo), referenceProfile, output,
List.of(dexInfo.dexPath()), options)) {
return ProfilePath.tmpProfilePath(output.profilePath);
}
return null;
}安装时若已有本地 profile,ART 可把 cur 与 reference 合成新的临时 profile;成功后才提交。非 pre-reboot 场景下,merge 成功还会删除已经纳入合并的 cur profile,以减少 runtime 下一次保存的增量数据;这只是优化,不改变下一次 profman merge 的正确性。
9. 测试给出的边界
源码文件:art/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java,相关测试:profile snapshot 与 embedded profile 测试
var config = DexMetadataConfig.newBuilder()
.setEnableEmbeddedProfile(false).build();
doReturn(new ZipFile(dmPath))
.when(mDexMetadataHelperInjector).openZipFile(any());
mArtManagerLocal.snapshotAppProfile(
mSnapshot, PKG_NAME_1, null /* splitName */);
verify(mArtd, never()).mergeProfiles(
argThat(profiles -> profiles.contains(embeddedProfile)),
any(), any(), any(), any());该测试把 config.pb 的 enableEmbeddedProfile 设为 false,验证 embedded profile 不进入 snapshot merge。它证明配置位会改变 profile 输入集合,不证明所有云端 profile 都来自 embedded profile。
源码文件:art/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java,相关测试:cloud reason 结果构造与 cleanup
doReturn(createGetDexoptStatusResult(
"speed-profile", "cloud", "location"))
.when(mArtd).getDexoptStatus(any());
verify(mArtd).cleanup(any() /* profilesToKeep */, any());测试夹具使用 cloud 作为 ART 统计 reason,并验证 cleanup 的调用参数;这只能证明 ART Service 支持 cloud reason 的状态记录和清理路径,不能把测试夹具解释为设备端自动联网或 Play 服务行为。
10. 故障定位
遇到“DM 已下载但首次运行仍像没有 profile”时,按以下顺序检查:
- framework
DexMetadataHelper.validateDexPaths是否把.dm与正确的 APK/split 配对。 PackageInstallerSession是否把.dm暂存到目标代码路径旁,更新时是否被错误遗漏。- ART
DexMetadataHelper.getDexMetadataInfo返回的是TYPE_NONE、TYPE_ERROR还是含 profile 的类型。 config.pb是否关闭 embedded profile,externalProfileErrors是否有读取或初始化错误。Dexopter是否拿到有效ProfilePath;如果没有,日志应出现 “there is no valid profile”,filter 会降为verify。dex2oat的 reason 是否带-dm,以及DexoptResult的 actual compiler filter 和 status。- 若 dexopt 成功但后续 profile 消失,检查临时 profile 是否提交、merge 后
cur清理是否符合当前是否 pre-reboot。
这条路径的核心结论是:云端 profile 是一个可选的安装输入,不是安装成功的前置条件;.dm 配对、ZIP 读取、profile 初始化、merge 和 compiler filter 每一层都可能让它退化为 verify,而 PMS 通常只接收 ART 返回的结果和 warning。
