Skip to content

云端 profiles

追踪 Android 17 中外部 profile 从安装文件进入 ART、参与首次 dexopt 并影响编译结果的设备端路径。

基于android-17.0.0_r1
AndroidPMSDexOptARTProfiles

云端 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. 设备端边界 ​

设备端主链可以压缩为:

text
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

java
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

java
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

java
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

java
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

java
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

java
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

java
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

java
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

java
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 时的降级分支

java
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

java
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

java
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

java
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 测试

java
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

java
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”时,按以下顺序检查:

  1. framework DexMetadataHelper.validateDexPaths 是否把 .dm 与正确的 APK/split 配对。
  2. PackageInstallerSession 是否把 .dm 暂存到目标代码路径旁,更新时是否被错误遗漏。
  3. ART DexMetadataHelper.getDexMetadataInfo 返回的是 TYPE_NONE、TYPE_ERROR 还是含 profile 的类型。
  4. config.pb 是否关闭 embedded profile,externalProfileErrors 是否有读取或初始化错误。
  5. Dexopter 是否拿到有效 ProfilePath;如果没有,日志应出现 “there is no valid profile”,filter 会降为 verify。
  6. dex2oat 的 reason 是否带 -dm,以及 DexoptResult 的 actual compiler filter 和 status。
  7. 若 dexopt 成功但后续 profile 消失,检查临时 profile 是否提交、merge 后 cur 清理是否符合当前是否 pre-reboot。

这条路径的核心结论是:云端 profile 是一个可选的安装输入,不是安装成功的前置条件;.dm 配对、ZIP 读取、profile 初始化、merge 和 compiler filter 每一层都可能让它退化为 verify,而 PMS 通常只接收 ART 返回的结果和 warning。