Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Baseline、Startup 与 Cloud Profile 编译优化

Baseline Profile 和 Startup Profile 由应用构建流程提供热路径,Cloud Profile 由分发侧根据真实使用生成,DM(DexMetadata)文件把 Profile 随安装包交付给 ART。三者能否兑现成启动收益,取决于安装后的编译状态和代码版本匹配,下面我们沿构建侧到设备侧把这条链路完整走一遍。

应用构建侧:生成、打包与基准验证

范围

Baseline Profile 解决的是代码在用户设备上“何时以什么编译状态运行”。没有预编译时,ART 可以先解释执行 DEX 字节码,再由 JIT(Just-In-Time)在运行中编译热点;AOT(Ahead-Of-Time)则在代码运行前完成编译。

Baseline Profile 能让关键路径更早获得合适的编译状态;磁盘、网络、Binder、锁等待、业务初始化和首屏布局工作,仍要按各自的问题治理。

源码基线:Android 17 / API 37 / AOSP android-17.0.0_r1。

截至 2026-10-25,官方列出的最低推荐稳定组合是 AGP 8.0.0、Macrobenchmark 1.5.0 和 ProfileInstaller 1.4.1;Android Studio 的 Baseline Profile Generator 模板从 Android Studio Iguana / AGP 8.2 开始提供,Startup Profile 的 DEX layout 优化从 AGP 8.1 可用、AGP 8.3 起默认启用。AGP、Macrobenchmark、ProfileInstaller 和 Google Play 各自演进,我们要固定一组经过验证的版本,并写进实验记录。

Baseline/Startup Profile 只处理由 ART 管理的 DEX 代码;AutoFDO 用采样或硬件分支轨迹指导 LLVM/Clang 优化 native 可执行文件和共享库。OEM dexpreopt 则发生在系统镜像构建阶段,处理 boot classpath、system_server、系统组件和预装 APK。这三条链路相邻,预装应用可能同时受它们影响,但输入数据、消费者、产物位置和验证工具各走各的路。

1. Baseline Profile 是编译提示

1.1 Profile 规则包含什么

Profile 规则以 Human Readable Format(HRF,可读文本格式)记录,内容是类名和方法签名。方法规则前可带:

  • H:Hot,运行中频繁使用的方法;
  • S:Startup,启动阶段使用的方法;
  • P:Post-startup,启动后使用的方法。

构建工具会把应用与依赖库的规则合并,按 release 产物的混淆和优化结果重写,再生成 ART 可消费的紧凑二进制 profile。二进制规则会关联具体 DEX 的标识与校验信息;应用版本号本身不是匹配依据。重新构建、混淆或优化改变 DEX 后,旧规则可能失配,应重新生成、重写并对最终发布 DEX 验证;未改变的 DEX 不能仅因应用版本号变化就判定失配。

1.2 ART 侧的分工

ART 这一侧的分工是:profman 负责 profile 处理与合并,dex2oat 根据 compiler filter 和 profile 选择编译工作,源码入口见 dex2oat.cc。compiler filter 是 ART 的编译策略名称,决定编译范围和优化程度,后面的排查我们要反复和它打交道。

speed-profile 表示由 profile 指定热点、按需编译。看到这个 filter,我们不妨把它当成一组待核对的问题:

  • 整个应用是否都已 AOT(Ahead-Of-Time,运行前预编译);
  • 每条规则是否仍与当前 DEX 匹配;
  • 静态初始化代码是否还要执行;
  • 类加载、验证和页面创建成本是否消失;
  • 所有安装渠道是否在同一时点完成编译。

Profile 改变的是代码执行前的准备状态:方法内部逻辑照样执行,I/O、锁和网络照样等待。

1.3 构建到设备的链路

下面按产物形态列出从生成规则到设备编译的各层。AGP 是 Android 构建插件,D8 把 Java/Kotlin 编译结果转换成 DEX,R8 负责代码缩减、混淆和优化:

BaselineProfileRule / 库规则
        ↓
HRF baseline-prof.txt(H/S/P + 类/方法)
        ↓  AGP + D8/R8 重写、合并、编码
APK/AAB 中的 binary baseline.prof
        ↓  Play / package installer / ProfileInstaller
设备 profile
        ↓  install-dm 或后台 dexopt
ART profile-guided 编译产物

这张图表达的是分层关系,具体时间线因渠道而异。Google Play 或支持 DexMetadata 的安装器可以提供 reference profile(作为编译基准安装的 profile),并以 install-dm 等原因触发编译;ProfileInstaller 则把包内规则写入设备可持续更新的 current profile,等待后台 dexopt(DEX 优化/编译流程)。dexopt 是系统对 DEX 做验证、编译和优化的过程。

图中的每一层都需要单独验证:源码里有 baseline-prof.txt,release 包未必带着它;包里有 baseline.prof,设备也未必已完成由 profile 指导的编译。

2. 四种 profile 不要混用

类型生产者主要用途可用边界
Baseline Profile应用/库作者与 CI引导 ART 编译常用路径Android 7 / API 24+
Startup Profile应用的启动生成场景构建期优化 DEX 布局依赖 AGP/R8 工具链
Cloud ProfileGoogle Play 聚合真实使用数据补充安装/更新时的 PGOAndroid 9 / API 28+、Play 渠道
本地运行时 profile单台设备的 ART/JIT随用户行为更新热点使用后逐步积累

CI 是持续集成系统,用来在每次代码变化后自动构建和测试。Baseline Profile 由团队选定 CUJ(Critical User Journey,关键用户路径)生成,能随新版本交付;Cloud Profile 要等待真实数据聚合,官方文档给出的传播时间是数小时到数天,还要求应用有足够用户规模。两者都属于 PGO(Profile-Guided Optimization),也就是用运行行为 profile 指导编译优化,会共同影响 Play 用户的编译状态。

Startup Profile 是 Baseline Profile 的启动子集,但消费方不同:Baseline Profile 给 ART,Startup Profile 给构建工具做 DEX 布局。它们可以来自同一生成脚本,验证方法却各有各的一套。

Android 7 / API 24 已经具备混合执行、JIT、本地 profile 与后台 profile-guided dexopt 的协作;Google Play Cloud Profile 则要到 Android 9 / API 28 才出现。所以 Android 7~8.1 的包内 Baseline Profile 通常要由 ProfileInstaller 写入 current profile 后等待后台编译;Play 用户仅凭安装完成,也还断定不了 Cloud/Baseline Profile 已变成 speed-profile 产物。

3. 什么场景可能受益

TTID 是首帧首次显示所需的时间,TTFD 是应用达到完整可用状态所需的时间。一个启动问题该不该往 profile 方向查,先对照这些现象:

  • 新装/升级后的启动明显慢于稳定使用后的启动;
  • Trace 中启动阶段有解释执行、JIT 编译、类加载和大量首次执行;
  • Compose、反射路由或大依赖链带来较多关键方法首次执行;
  • 首次进入高频页面时出现代码执行相关抖动;
  • 库升级后 profile 覆盖率明显变化。

这些现象则通常出在 profile 之外:

  • 主线程同步文件或数据库升级;
  • Binder 跨进程调用或锁等待;
  • 网络结果成为 TTID/TTFD 硬依赖;
  • 图片解码、复杂布局或过多 Compose 组合(根据状态构建界面树的过程);
  • SDK 在 Provider/Application 中执行重活;
  • Main(UI 主线程)/RenderThread(负责部分渲染工作的线程)被大量后台任务抢占。

先用 启动完整路径分析确认瓶颈。Baseline Profile 应与初始化、I/O 和渲染修复一同评估;“后几次会变快”掩盖不了首次运行的问题。

4. 生成:场景决定 profile 质量

4.1 使用 Baseline Profile Gradle Plugin

推荐用 Android Studio 的 Baseline Profile Generator 模板或 Baseline Profile Gradle Plugin。generator module 是一个独立的设备测试模块,负责启动目标应用并采集规则;应用模块通过下面的依赖声明消费它:

plugins {
    id("com.android.application")
    id("androidx.baselineprofile")
}

dependencies {
    baselineProfile(project(":baseline-profile"))
}

插件负责把构建变体(variant)与生成任务关联起来,合并规则,并按 release 产物的混淆结果重写类名和方法名。具体插件版本应与项目 AGP、Macrobenchmark 和 ProfileInstaller 组成经过验证的工具链;升级其中一项后需要重新生成和测量。

同一条生成链路上有三个角色,模块边界比把规则文件直接复制进应用更重要:

角色典型模块负责什么不负责什么
producer独立 com.android.test 的 :baseline-profile在受控设备上驱动 CUJ,输出 HRF 规则不承载生产业务代码
consumer最终 :app合并应用与依赖库规则,由 R8 按 release 符号重写并打包不用生成 variant 的未混淆 DEX 代替发布 DEX
libraryAAR 与 sample app通过真实公开 API 生成并过滤本库规则不知道宿主启动入口,不能贡献最终 Startup Profile

producer 的生成变体应保持 debuggable=false、profileable=true、不混淆且不优化;最终 release 应启用 R8。API 33+ 可在非 root 设备生成规则,API 28~32 需要 rooted 环境;生成环境只决定能否收集,收益结论仍要回到目标物理设备和 release 类产物。

4.2 启动场景要显式加入 Startup Profile

下面的 generator 覆盖从桌面图标进入首页的路径,并明确把该场景加入 Startup Profile:

@LargeTest
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun launcherToHome() = rule.collect(
        packageName = TARGET_PACKAGE,
        includeInStartupProfile = true,
        strictStability = true,
    ) {
        pressHome()
        startActivityAndWait()

        check(
            device.wait(
                Until.hasObject(By.res(TARGET_PACKAGE, "home_list")),
                5_000,
            )
        )
    }
}

strictStability = true 只管规则稳定性这一层:稳定性检查失败时采集会跟着失败,页面就绪和业务数据仍要靠断言保证。

includeInStartupProfile = true 只给初始显示必经的启动场景用。滚动、详情、搜索这类 CUJ 常用归常用,放进 Baseline Profile 就够了;全塞进 Startup Profile 会扩大启动 DEX 布局范围。

4.3 覆盖所有主要启动入口

采集场景同时校验页面与启动范围

图:生成稳定性与页面正确性分别检查;非启动 CUJ 仍可贡献 Baseline Profile。

如果只覆盖从桌面图标(launcher)启动的场景,还应补充独立且确定的场景:

  • 已登录与未登录;
  • deep link(从网页或其他应用直达指定页面)到高频页面;
  • 通知点击;
  • 恢复保存状态;
  • Compose 首次进入;
  • 动态特性模块已安装/未安装的允许路径;
  • 主要 product flavor,例如 free/paid 或不同渠道配置。

每个场景都要固定账号、隐私弹窗、远程配置、语言、数据和网络响应。waitForIdle() 只说明界面线程暂时空闲,页面内容对不对还得靠断言:测试应检查业务 UI 或状态。脚本走错页面时应立即失败,避免把错误路径写进 profile。

4.4 启动之外的 CUJ

高频滚动、导航和交互可用另一条 collect 场景生成 Baseline Profile 规则,并保持 includeInStartupProfile = false。这样 ART 仍可编译相关方法,R8 不会把它们当成启动布局依据。

库可以随 AAR(Android Library Archive,Android 库包)提供 Baseline Profile,应用构建时合并这些规则。最终规则和包体仍要由应用侧检查:库作者并不知道宿主的启动入口,Startup Profile 也只能由宿主自己贡献。

5. 生成产物要可复现

Baseline Profile Gradle task 的保存位置由 saveInSrc 决定。设为 true 时,生成结果会复制到 src/<variant>/generated/baselineProfiles source set;source set 是只属于某个构建变体的一组源码和资源。设为 false 时,结果只保留在 build 目录的中间产物中。团队可以选择:

  • 将生成规则纳入版本控制并审查每次差异;
  • 或由固定设备镜像与工具链在 CI 重建,保存产物和差异报告。

两种方式都要记录:

  • generator 代码与测试数据版本;
  • 设备/API 和 Macrobenchmark 版本;
  • AGP/R8/ProfileInstaller 版本;
  • 目标构建变体与应用源码版本;
  • 生成规则增删数量和异常大范围变化。

模板中的 profile 生成变体通常不做混淆和优化,release 则开启 R8。Baseline Profile Plugin 与 AGP 负责把可读规则重写为优化后 release DEX 中的类名和方法名。绕开插件手工复制时,签名可能不匹配。

一次 generator 跑成功,离 profile 正确还有距离:规则异常变大、入口漏掉、测试误入错误页,都要在构建阶段拦下,阻止发布。

6. 检查 APK/AAB

官方调试指南 给出的关键位置是:

  • APK:/assets/dexopt/baseline.prof
  • AAB:/BUNDLE-METADATA/com.android.tools.build.profiles/baseline.prof

APK 是可安装包,AAB(Android App Bundle)是交给应用商店生成设备专用 APK 的发布包。下面的命令用于确认二者是否包含二进制 profile:

unzip -l app-release.apk \
  | rg 'assets/dexopt/baseline\.prof$'

unzip -l app-release.aab \
  | rg 'BUNDLE-METADATA/com\.android\.tools\.build\.profiles/baseline\.prof$'

没有输出时,检查构建变体、source set、generator 依赖和 AGP 任务。编译后的 baseline.prof 必须小于 1.5 MB;这个限制不适用于通常更大的 HRF 文本规则。工具链还可能打包 baseline.profm,它保存 profile 格式转换所需的元数据,以便 ProfileInstaller 适配不同 ART 版本。验收仍要以当前 AGP 与目标设备格式为准,扩展名本身只是一个线索。

库 profile 也要在最终应用产物中验证:AAR 自身有规则只完成了一半,宿主所用 AGP 有没有正确消费,要看最终包。

7. 安装与编译状态

7.1 渠道决定时机

Android 7~8.1 主要依靠 ProfileInstaller 在首次运行后写入 current profile,再等待后台 dexopt。Android 9+ 可以由 Google Play 或支持 DexMetadata 的 package installer 交付 reference profile。当前官方调试文档指出,Google Play 的 Baseline Profile 编译可能发生在后台设备更新;APK 安装命令返回时编译状态如何,要另查。

侧载、第三方商店、Android Studio、Gradle 和 Play 各有各的行为,互相替代不了。其他安装工具通常由 ProfileInstaller 把规则排入下一次后台 dexopt;非 Play 渠道也可能要等设备空闲时才获得编译收益。AGP 8.4+ 会让 Android Studio 或 Gradle Wrapper 安装的本地不可调试构建(non-debuggable)自动执行设备端编译,但发布验收仍要覆盖目标渠道。

7.2 ProfileVerifier 能说明什么

安装渠道决定 Profile 到达哪个阶段

图:ProfileInstaller、支持 DM 的安装器以及允许读取嵌入规则的 ART 可走不同路径;文件交付与编译产物仍需分别验收。

ProfileVerifier可以报告:

  • APK 内没有嵌入 profile;
  • 包内有 profile,但设备尚未安装可用 profile;
  • profile 已入队等待编译;
  • 已按 profile 编译;
  • 已使用不完全匹配的 profile 编译;
  • 不支持的 API 或缓存/包错误。

RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED 表示包内没有规则;ProfileInstaller 1.4.1 起推荐看 RESULT_CODE_NO_PROFILE_INSTALLED(旧名 RESULT_CODE_NO_PROFILE,已废弃),它通常表示包内有规则,但 ProfileInstaller 没有运行或设备尚无已安装 profile。当前编译用的是 Baseline Profile 还是 Cloud Profile,ProfileVerifier 分不出来。RESULT_CODE_COMPILED_WITH_PROFILE 是检查器推断出的 profile 编译状态,不说明全部代码已经 AOT。固定到 ProfileInstaller 1.4.1 源码,它主要根据 current/reference profile 是否存在、文件大小与缓存状态判断,并未逐方法检查 AOT 产物;COMPILED_WITH_PROFILE_NON_MATCHING 也使用文件大小变化的启发式,源码明确指出可能误判。需要确认当前产物和 filter 时,还应结合 pm art dump;DEX checksum、方法覆盖率和性能收益要分别验证。

ProfileInstaller 1.4.1 的 ProfileVerifier 支持范围有一个例外:API 28、29 及 API 31+ 可检查,API 28 以下和 Android 11 / API 30 都返回 RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION。writeProfileVerification() 会执行 I/O,应在后台线程调用。若关闭 ProfileInstallerInitializer,需要在启动数秒后手动调用该方法,否则 getCompilationStatusAsync() 返回的 Future(异步结果)可能一直等待或超时。

7.3 dumpsys package dexopt

下面的命令用于线下查看目标包的 compiler filter(当前编译策略)与 reason(触发原因)。它在旧版本 Android 上更适合辅助排查;新版本的首选入口则是后文的 pm art dump:

adb shell dumpsys package dexopt \
  | rg -A 4 'com\.example\.app'

status=speed-profile 表示存在由 profile 指导的编译产物;reason=install-dm、bg-dexopt 或 cmdline 分别表示安装期 DexMetadata、后台优化或命令行触发。至于哪条规则命中,它给不出逐方法的答案。

需要手工验证编译器能否消费已经安装的 profile 时,可使用:

adb shell cmd package compile \
  -f -m speed-profile com.example.app

固定到 Android 17 r1,ART Service 在配置允许时会尝试 APK 内嵌 profile,也会检查相邻 .prof 与 .dm;不应断言必须先由 ProfileInstaller 写入 current profile,手工编译才能使用包内规则。Utils.initReferenceProfile() 中嵌入式输入还受 API 35+ 与 DM 配置约束,非嵌入式外部输入优先。较低系统版本不能直接套用这条行为。Macrobenchmark 会按 CompilationMode 准备 profile 和编译状态,自动化性能实验优先使用它。

8. 用 Macrobenchmark 测 profile 差值

8.1 固定编译模式

下面的两个测试使用同一应用、入口和场景,只改变编译模式:

@LargeTest
@RunWith(AndroidJUnit4::class)
class BaselineProfileStartupBenchmark {
    @get:Rule
    val rule = MacrobenchmarkRule()

    @Test
    fun noPreCompilation() = measure(
        CompilationMode.None(),
    )

    @Test
    fun baselineProfile() = measure(
        CompilationMode.Partial(
            baselineProfileMode = BaselineProfileMode.Require,
        ),
    )

    private fun measure(mode: CompilationMode) = rule.measureRepeated(
        packageName = TARGET_PACKAGE,
        metrics = listOf(StartupTimingMetric()),
        compilationMode = mode,
        startupMode = StartupMode.COLD,
        iterations = 10,
        setupBlock = {
            pressHome()
        },
    ) {
        startActivityAndWait()
    }
}

BaselineProfileMode.Require 在产物缺少或无法安装 profile 时让测试失败。固定到 Macrobenchmark 1.5.0,Partial() 的默认参数本来就是 Require;这里显式写出以固定实验意图。只有主动选择 UseIfAvailable 时,安装失败才允许继续无 Baseline Profile 的条件。官方 API 还要求被测应用 APK 包含 ProfileInstaller,并由 AGP 7.0+ 打包 Baseline Profile;这里指目标 App,不是承载测试代码的 instrumentation APK。十次迭代只是起点,我们应按设备噪声和希望识别的最小回归幅度决定样本量。

8.2 正确解释 None 与 Partial

CompilationMode.None() 构造无预编译的受控状态,适合测量 profile 能提供的本地差值,但运行期间仍允许 JIT;它不是禁用 JIT 的纯解释执行模式。线上“未配置 Baseline Profile”的用户可能还有 Cloud Profile、本地 JIT profile 或后台 dexopt,这种受控状态代表不了他们的全部情况。

CompilationMode.Partial(...Require) 测量 Baseline Profile 已准备并参与部分编译的状态。CompilationMode.Full() 可作为全面方法 AOT 编译的对照;它不保证耗时最短,也不预先执行类的静态初始化。全面编译的状态代表不了生产安装常态。

8.3 同时观察这些指标

  • TTID 与 TTFD;
  • 首屏和高频 CUJ 的 FrameTiming 与 Jank(一帧错过显示期限造成的卡顿);
  • 主线程 Running(正在 CPU 上执行)/Runnable(可以执行但在等 CPU)、类加载、JIT 和垃圾回收;
  • 安装/编译状态与产物;
  • APK/AAB 大小和编译产物带来的设备磁盘成本;
  • 崩溃、ANR(应用无响应)与功能正确性。

如果 None 与 Partial 几乎相同,打开 Trace 判断:

  • 测试产物是否带 profile;
  • Profile 是否覆盖当前入口;
  • R8 重写后规则是否匹配;
  • 关键路径是否已经转移到 I/O、锁、Binder 或渲染;
  • 测试是否被缓存、弹窗或错误页面污染。

9. Startup Profile 与 DEX Layout

9.1 构建期职责

Startup Profile 让 R8/D8 调整类和方法在 DEX 文件中的顺序与分布,把初始显示所需代码优先放进启动 DEX 或相邻区域。这样可以提高加载局部性,也就是一次文件映射或读取更容易覆盖接下来要执行的代码。它影响的是代码排布;ART 编译状态仍由 Baseline Profile 那条链路负责。

使用要点:

  • 启动 generator 设置 includeInStartupProfile = true;
  • release 开启 R8 与优化;
  • AGP 8.1~8.2 显式启用 DEX layout;
  • AGP 8.3+默认启用相关优化;
  • 不把非启动 CUJ 全塞进 Startup Profile;
  • 所有主要启动入口都由应用 generator 覆盖。

启用 saveInSrc 时,生成后的规则通常位于 src/<variant>/generated/baselineProfiles/startup-prof.txt,由 AGP 自动消费。官方文档同样明确:库不知道宿主的入口、主 DEX 容量和完整调用路径,Startup Profile 必须由宿主应用自己贡献。

9.2 验证方式

Startup Profile 把启动代码集中到合适布局

图:这是文件布局示意,不表示全部启动类一定只占一个 DEX,也不承诺 CPU cache 命中。

Startup Profile 在构建时应用,APK/AAB 中没有供 ART 使用的独立 startup.prof。验证方式包括:

  • AGP 8.8+检查 AAB 中 r8.json 的 startup DEX 标记;
  • 用 APK Analyzer 检查 classes.dex 是否包含主要启动类;
  • 比较 DEX 数量、布局和 page fault;文件映射的 page fault 表示当前访问需要建立或补齐页映射,其中 minor fault 可能直接使用已有页缓存,并不一定触发存储 I/O;
  • 用同编译模式 Macrobenchmark 比较 TTID/TTFD。

“类靠近就一定命中 CPU cache”这样的收益表述并不成立:这里优化的是 DEX 文件布局和读取局部性,运行时结果仍受设备、文件映射、编译状态与代码路径影响。

9.3 证明 release 产物保留了布局

只看到仓库里存在 startup-prof.txt 还不够,应逐层回答生成、消费和发布产物是否一致:

证据能证明什么不能证明什么
startup-prof.txt 及其差异generator 采到了哪些启动规则R8 已消费规则
release 的 R8 配置与构建日志构建具备 DEX layout 条件最终 DEX 未被后处理改写
APK Analyzer 中的 classes.dex关键启动类实际位于哪个 DEX设备端已经按 profile 编译
AAB 的 BUNDLE-METADATA/com.android.tools/r8.json哪些 DEX 被标记为 startup DEXmetadata 一定对应最终文件
metadata checksum 与 DEX SHA-256R8 后的 DEX 没有被加固、插桩或重打包悄悄替换布局一定带来可测收益

checksum 是用于判断文件内容是否变化的摘要。AGP 8.8+ 可以从 AAB 的 r8.json 构建元数据中检查至少一个 DEX 是否带有 "startup": true,并将其中 checksum 与包内对应 DEX 的 SHA-256 摘要对齐。两者不一致时,应先定位 R8 之后的处理步骤;依据旧元数据宣称发布包保留了布局,是站不住的。表中的“加固”指 R8 后对应用包追加保护处理,“插桩”指向 DEX 插入监控或统计代码。

主 DEX 容量也是约束。规则过宽时,初始显示以外的 CUJ 会挤占 classes.dex,必要的启动代码反而溢出。单 DEX 应用因布局优化变为两个 DEX 不一定是回归:构建工具可能把启动代码集中在主 DEX,并把非启动代码移到后续 DEX。判断依据应是启动类分布、R8 诊断和测量结果,光看 DEX 个数会误判。

9.4 用单变量 A/B 隔离布局收益

DEX layout A/B 的源码、资源、R8 规则、签名配置、Baseline Profile、安装步骤和设备端 CompilationMode 必须一致,唯一变量是是否向 D8/R8 提供 Startup Profile。若一组同时移除 Baseline Profile,测到的差异就混在一起,分不清 ART 编译和 DEX 布局各自的贡献。

P50 是中位数,P90/P95 分别表示 90%/95% 的样本不超过该值。实验至少记录 TTID、TTFD、P50/P90/P95、数据波动范围和失败样本,并用 Perfetto 核对:

  • DEX 映射、文件读取和缺页次数是否减少且趋于稳定;
  • 类加载区间是否缩短;
  • 两组 JIT/AOT 状态是否一致;
  • Binder、SQLite、锁、网络、资源解码是否掩盖布局收益;
  • 反射、动态 DEX 或条件分支是否让关键代码没有进入规则。

手动杀进程只重建进程状态,设备 page cache 未必跟着变冷——page cache 是内核缓存的文件页,应用进程退出后仍可能保留。Macrobenchmark 1.5.0 的 StartupMode.COLD 会依次清 shader cache、杀目标进程、调用 dropKernelPageCache();API 31+ 的后者走不需要 root 的平台入口,旧设备按 root 能力处理或记录未清缓存的警告。应归档实际成功与否和版本;这些步骤仍不能证明存储控制器的所有缓存都处于冷态。

Macrobenchmark 1.5.0 的 COLD 准备顺序

图:固定版本实现依次执行这些步骤;API 31+ 清页缓存不要求 root,旧设备按能力处理。

9.5 常见失效模式

  • 规则过宽:主 DEX 接近容量上限,非首屏 CUJ 挤占布局预算;缩小到初始显示路径。
  • 生成环境漂移:弹窗、实验、账号和网络分支让同一源码版本产生不同规则;固定测试环境并审查差异。
  • 构建后改写 DEX:r8.json checksum 与最终 DEX 不一致;修复加固、插桩或重打包链路。
  • 编译状态不一致:实验组的 compiler filter 不同;固定编译模式后再比较布局。
  • 瓶颈不在 DEX:Trace 显示主要时间落在 Binder、数据库、锁或网络;保留 Profile,同时治理当前关键路径。

10. 维护与回归

这些变化应触发 profile 重新验证:

  • 启动 Activity、deep link、导航或登录流程改变;
  • Compose、路由、DI(依赖注入)、数据库或大 SDK 升级;
  • 包名、模块、动态特性、product flavor 或 ABI(应用二进制接口)改变;
  • R8/AGP/Kotlin/Compose 编译器升级;
  • reportFullyDrawn() 完成条件改变;
  • 新增高频 CUJ 或删除旧功能;
  • profile 规则数量、大小或覆盖率异常变化。

CI 至少执行:

  1. generator 场景断言;
  2. profile 规则差异与大小检查;
  3. release APK/AAB profile 路径检查;
  4. CompilationMode.Partial(BaselineProfileMode.Require) 基准;
  5. None 对照与回归阈值;
  6. 选定设备档位的 TTID/TTFD 和帧结果归档。

如果回归来自 I/O、锁、网络或任务编排,回到 启动任务编排和 ContentProvider 启动治理。Profile 是编译侧工具,业务关键路径的取舍还得回到业务本身。

检查清单

  • 生成工具链、设备/API、构建变体和源码版本可追溯。
  • launcher、deep link、登录态等主要入口有确定脚本。
  • 只有初始显示场景进入 Startup Profile。
  • 应用和库 Baseline Profile 已在最终 release 合并。
  • APK/AAB 中存在目标构建变体的 baseline.prof。
  • 安装渠道、ProfileInstaller 和编译状态已分别验证。
  • Macrobenchmark 显式使用 BaselineProfileMode.Require。
  • None/Partial 结果在相同设备与数据条件下比较。
  • TTID、TTFD、帧、稳定性与功能同时通过。
  • Startup Profile 的 DEX 布局用构建产物和 Trace 验证。

分发与设备侧:Cloud Profile、DM 与 dexopt

应用构建侧负责生成规则、证明本地收益;分发与设备侧要回答的是:这些输入有没有到达目标 APK、有没有被 ART 接受、何时形成可用的编译产物。Cloud Profile 补充真实用户热路径,DM 只是随 APK 交付元数据的容器,设备最终做了什么,要看 compiler filter 与 reason。

Profile 在发布后如何生效

前半篇已经完成 Baseline/Startup Profile 的生成、打包与受控基准;从这里开始,我们追踪发布后的安装来源、外部 profile、DM、ART Service 与 dexopt 状态。“规则打包成功”和“设备已按规则编译”要分开证明,同一条依据兼顾不了两件事。

Profile 能减少的是解释器逐条执行、JIT 预热和部分 DEX 读取的开销;数据库迁移、网络等待、锁竞争、SDK 同步初始化该等多久还是等多久。一次冷启动混着这些成本,只看总耗时很容易误判。

云端 Profile 在启动优化里的位置

从 Android 7 起,ART 会在解释执行、JIT 和 AOT 产物之间选择。设备运行 App 时,本地 profile 逐渐记录常用代码;后台 dexopt 再按 profile 编译高频路径。Baseline Profile 和 Cloud Profile 都能在本机运行数据尚未积累充分时提供编译依据,但两者的生产者、可用时机和可控程度不同。

Profile 类型生产者何时生效主要作用App 团队能否直接控制
Baseline ProfileApp 或库的开发团队随发布产物交付,安装期或后续 dexopt 使用覆盖新安装、新升级后的核心路径可以;规则、版本和回归测试都由发布方维护
Cloud ProfileGoogle Play 根据已发布版本的真实用户样本聚合样本达到条件并由 Play 交付之后补充 Baseline Profile 未覆盖的高频路径不能直接编辑;依赖 Play、版本规模和聚合周期
本地运行时 profile单台设备上的 ART/JIT用户运行 App 后逐步积累指导该设备后续的 speed-profile 编译只能通过稳定的代码路径间接影响
Startup ProfileApp 团队与构建工具构建期由 D8/R8 消费调整启动相关类和方法在 DEX 中的排列可以;它不作为设备端 AOT profile 使用

Google 的 Baseline Profiles 文档给出了两条重要事实:

  • Cloud Profile 属于 PGO(Profile-Guided Optimization,按真实运行特征优化),需要 Android 9(API 28)或更高版本、足够的用户样本以及聚合时间;新版本发布后的数小时到数天内,要按尚未可用来安排验证。
  • Baseline Profile 随当前版本发布,可以覆盖 Day-0,也就是用户安装或升级这个版本后的首次使用阶段,Cloud Profile 赶不上这个时点,替代不了它。

因此我们要把 Baseline/Cloud Profile 与 Startup Profile 分开评估。前两者主要影响哪些方法被 AOT 编译;Startup Profile 由 D8/R8 在构建期使用,主要改变 DEX 布局,目标是减少启动阶段分散读取文件和触发内存缺页的成本。一次构建可以同时使用两类规则,但 A/B 实验(把条件相同的用户随机分组对比)需要明确改变的是编译状态、DEX 布局,还是两者一起改变。

DM 文件与 ART 编译模式

.dm 是 Dex Metadata 容器,与某个 APK 一一对应。framework 的 android.content.pm.dex.DexMetadataHelper 把 base.apk 映射为同目录下的 base.dm;split APK(App Bundle 拆出的功能或配置 APK)也按各自文件名匹配,base 的 .dm 顶替不了 split 的 metadata。

关于 .dm,先澄清三件事:

  1. .dm 和 Cloud Profile 是两回事。它是 ZIP 容器,可以带 profile,也可以带 VDEX(ART 使用的 DEX 校验相关数据)和配置;是否来自 Play、包含什么内容,要结合安装来源和文件内容判断。
  2. .dm 未必每次安装都在。APK 内嵌 Baseline Profile、相邻的 base.apk.prof 和相邻的 base.dm 都可能成为 ART Service 的外部输入。
  3. 文件存在离 profile 可用还差一步。文件名、二进制格式或 DEX checksum(用于确认 profile 对应同一份 DEX 的校验值)不匹配时,ART Service 会拒绝该 profile。

这些判断在源码里都有直接依据:

  • PrimaryDexUtils.getExternalProfiles() 同时返回 prebuilt profile(与 APK 相邻的 <apk-name>.prof)路径和 .dm profile 路径。
  • ART Service 的 com.android.server.art.DexMetadataHelper.getDexMetadataInfo() 打开 .dm,读取可选的 config.pb,并按只含 profile、只含 VDEX、两者都有或都没有这几种情况分类。
  • PrimaryDexopter.buildDmPath() 按当前 base/split APK 构造 .dm 路径。
  • Dexopter 只有在存在有效 profile 时才保留 profile-guided filter;profile 为空时,会把请求的 speed-profile 调整为 verify。

framework 的安装流程会先检查每个 .dm 是否有同名 APK 与之配对。完成路径匹配后,android.content.pm.dex.DexMetadataHelper.validateDexMetadataFile() 只确认 .dm 能作为 ZIP 归档打开;它的注释仍提到校验 manifest.json 中的包名和版本号,但 android-17.0.0_r1 的方法体没有执行这项语义校验。

ART 在消费 profile 时还会检查 profile 格式和 DEX 匹配关系;安装期的检查和编译期的检查是两个阶段,要分别看各自的结果。

PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE 也能说明安装期与后续 dexopt 是两个时机:该安装标志会在安装时忽略 .dm 和 APK 内嵌 profile,并不报告无效安装 profile 的警告;以后由后台 dexopt 或 pm compile 发起的编译仍可使用 profile。

从安装到编译的执行链路

下面的路径图用于区分输入、调度者和编译执行者。Package Manager 负责安装流程,ART Service 负责制定这次 dexopt 参数,artd 是执行 ART 文件与编译操作的系统守护进程,dex2oat 则把 DEX 编译为设备可执行的 ART 产物。

APK / split APK
  ├─ APK 内嵌 Baseline Profile
  ├─ 相邻的 <apk-name>.prof
  └─ 相邻的 <apk-stem>.dm
             │
Package Manager ──发起安装期 dexopt──► ART Service
                                           │ 选择 profile、reason、filter
                                           ▼
                                         artd
                                           │
                                           ▼
                                        dex2oat

运行时 JIT profile ──后台维护窗口──────────► ART Service

从 Android 14(API 34)起,App 的设备端 AOT 编译由 ART Service 管理。installd/dexopt.cpp 对理解 Android 13 及更早版本有历史价值,但已不是 Android 17 的主调度链路;核对 Android 17 行为时,应查看 ArtManagerLocal、Dexopter、PrimaryDexopter、artd 和 dex2oat。

compiler filter 与 compilation reason

这两个字段回答不同问题。compiler filter 是编译器工作到什么程度,compilation reason 是这次工作为什么发生:

  • compiler filter 表示“生成了何种编译产物”。
  • compilation reason 表示“哪类事件触发了这次 dexopt”,例如安装、后台维护或 shell 命令。
filterAndroid 17 中的含义代价与边界
verify验证 DEX,不生成 AOT 机器码CPU、存储成本较低;启动路径更多依赖解释器与 JIT
speed-profile只 AOT 编译有效 profile 选中的方法成本取决于 profile;没有有效 profile 时,实际 filter 会变成 verify
speed面向速度 AOT 编译全部方法编译耗时和产物体积更高,不适合作为普通第三方 App 的默认对照目标

排查时必须同时记录 requested filter(请求的编译强度)和 actual filter(ART 最终采用的编译强度)。命令参数 -m speed-profile 只说明我们请求了什么,profile 是否命中要看 actual:Dexopter.java 在找不到有效 profile 时会把它改为 verify。

dex2oat、bg-dexopt-job 与空闲编译

dex2oat 是 ART 的 DEX 编译器,bg-dexopt-job 是系统安排的后台 DEX 优化任务。安装返回成功之后,编译未必就此结束:安装期若没有用上有效 profile,App 运行后仍可由 JIT 采集本地 profile,后台任务再把这些信息用于 speed-profile 编译。所以同一 APK 的“安装后第一次打开”和“使用数日后的打开”,完全可能处于不同编译状态。

ART Service 的标准默认值包括:

  • pm.dexopt.first-boot=verify
  • pm.dexopt.boot-after-ota=verify
  • pm.dexopt.boot-after-mainline-update=verify
  • pm.dexopt.bg-dexopt=speed-profile
  • pm.dexopt.inactive=verify
  • pm.dexopt.cmdline=verify

共享代码的规则需要按版本核对。某个包的代码被其他 App 使用时,本地用户 profile 不能进入对其他应用公开的优化产物。固定到 Android 17 r1 的 Dexopter,profile-guided 编译先尝试外部 profile,以便生成可共享产物;没有可用外部输入时,仍可使用本地 profile,但优化产物只供本 App 使用,公开的 VDEX 可让其他 App 走 verify。最终连本地 profile 也没有才退为 verify。当前 ART Service 配置文档还描述 pm.dexopt.shared 的历史回退策略,不能直接把那条概述当成本版本的实现分支;设备实际 filter 和文件权限仍应实测。

Android 14 及更高版本中,后台 dexopt 默认每天在设备空闲且充电时运行。设备退出 idle(无人操作的空闲状态),或温度达到取消阈值时,任务会取消;官方文档给出的默认温度阈值为 THERMAL_STATUS_MODERATE,系统配置可以调整。ART Service 没有旧 Package Manager 的 post-boot(开机后)dexopt job,旧版本的开机后前台资源竞争直接套不到 Android 17 上。

Android 17 共享代码的 Profile 与产物权限

图:外部输入优先;使用本地用户 profile 时优化产物保持私有,公开 VDEX 可供其他 App 走 verify。

普通 App 进程无权要求用户设备立即完成后台 dexopt。pm.dexopt.disable_bg_dexopt 和 pm bg-dexopt-job --disable 是系统开发者用于测试的入口,第三方 App 的线上优化方案里用不上它们。

查看包的 dexopt 状态

在 Android 14—17 测试设备上,我们用下面的命令读取目标包当前的主 DEX 与 split DEX 编译状态。

adb shell pm art dump com.example.app

输出中要逐个 APK/ABI 查看 compiler filter、compilation reason 和 artifact(编译产物)状态。ABI 表示 CPU 指令集与二进制接口,例如 arm64-v8a。Android 13 及更早版本可用 dumpsys package dexopt 辅助排查;Android 17 的首选入口是 pm art dump。

下面的属性采集用于记录 ROM(设备厂商定制的系统软件)对标准策略的修改,仅适合测试环境。

adb shell getprop | rg 'pm\.dexopt|dex2oat'

这些值由系统镜像和 ART 模块决定。不同设备出现差异时,把它们作为实验条件保存下来即可;据此要求线上 App 去改系统属性,是找错了方向。

Profile 命中率与冷启动收益评估

第一道检查:发布产物里有什么

我们先跑下面的命令,确认 APK 和 AAB 是否包含 Baseline Profile;规则覆盖率要另行评估。

unzip -l app-release.apk | rg 'assets/dexopt/baseline\.prof$'
unzip -l app-release.aab | rg 'BUNDLE-METADATA/com\.android\.tools\.build\.profiles/baseline\.prof$'

APK 和 AAB 的存放位置不同。看到条目说明打包成功;目标设备有没有按它完成编译,要靠后面的检查回答。

若测试材料已经包含匹配的 APK 与 .dm,下面的安装方式可以复现二者一起交付的路径。

adb install-multiple base.apk base.dm

.dm 必须与该 APK 版本匹配。这个实验能验证 Android 设备怎样消费外部 metadata;这个 .dm 是不是来自 Play 的 Cloud Profile,还要结合安装来源判断。

第二道检查:外部 profile 能否被 ART 使用

官方 ART Service 文档推荐先清除本地采集的 profile,再强制执行带详细结果的 speed-profile 编译。

adb shell pm art clear-app-profiles com.example.app
adb shell pm compile -m speed-profile -f -v com.example.app

第一条命令清除 current profile(ART 当前采集的运行记录)和 reference profile(已合并、供后续编译参考的记录),但保留 Cloud Profile 等 external profile(由 APK 或安装器从外部提供的 profile),已有编译产物也不会删除。第二条命令的判定依据是输出:actualCompilerFilter=speed-profile 表示找到了有效 profile;actualCompilerFilter=verify 表示没有可用 profile。典型原因包括文件名错误、格式错误和 DEX checksum 不匹配。

这个测试区分不了 APK 内嵌 Baseline Profile、相邻 .prof 与 .dm 中的 profile——它们对 ART 都是可选的外部输入。要区分来源,必须控制安装产物;actualCompilerFilter 只回答“有没有可用 profile”,回答不了“它从哪来”。

第三道检查:构造无 AOT 编译基线

下面的命令用于把包恢复到近似“新安装但未编译”的状态,适合在 Android 14—17 的命令行测试中建立基准组。

adb shell pm compile --reset com.example.app

Android 17 的 ArtShellCommand 对该命令有精确定义。它清除 current/reference profile;external profile 会保留给以后的 dexopt,但这次 reset(重置)不读取它们;主 DEX 当前等价于 verify,secondary DEX(运行期发现的附加 DEX)的 dexopt 产物则会被删除。该命令与“卸载并从某个商店重新安装”并不等价,因为安装来源、数据状态和交付文件都没有重建。

若只想生成不含 AOT 代码的产物而不清 profile,可以使用下面的命令。

adb shell pm compile -m verify -f -v com.example.app

它与 --reset 的差别在于不负责清除 profile。实验记录里要写清使用了哪条命令,否则后续 speed-profile 可能读取到上一轮留下的本地 profile。

ProfileVerifier 与 Macrobenchmark 的边界

ProfileVerifier 是 AndroidX 提供的 profile 状态检查器,报告范围限于单个 App 的 profile 安装与编译状态。ART 用的是 Baseline Profile 还是 Cloud Profile,它判断不了;方法覆盖率与启动耗时也另有专门的测量要做。

Macrobenchmark 的 CompilationMode.None 与 Partial 用来评估 Baseline Profile 的可控收益,更适合在本地复现。官方文档把这类本地结果视为 profile 可用时的理想场景,并明确指出它不包含生产设备上的 Cloud Profile 影响。验证 Cloud Profile 需要 Play 安装来源、同版本样本分组和足够的观察周期;一次本地 pm compile,撑不起 Cloud Profile A/B 实验的结论。

三组实验与观测字段

组别建议状态回答的问题
无 AOT 编译组pm compile --reset 后立即测量观察缺少 AOT 时的受控对照;不保证它是所有条件下最慢的一组
目标 profile 组控制交付文件,确认 actual filter 为 speed-profile当前外部 profile 能带来多少收益
设备稳态组固定版本运行代表性路径,等待后台 dexopt 后测量本地 profile 积累并完成编译后的性能

每组至少固定 APK/AAB、设备与系统 build(构建版本)、ART 模块版本、安装来源、账号与数据、网络条件、温度、启动脚本和启动类型。测量指标至少包含 TTID(首次显示时间)、TTFD(主要内容完整可用时间)、慢帧以及 P50/P90/P99(分别有 50%、90%、99% 样本不超过的耗时),并为每条样本附上:

  • 首次安装还是版本升级;
  • base 与各 split 的 compiler filter/reason;
  • 是否执行过 clear/reset/force compile;
  • 是否处于后台 dexopt 之后;
  • 是否清进程、清数据,以及是否清过文件页缓存。

“冷进程”与“冷文件页缓存”是两个条件。手动杀进程后,APK、DEX 和资源仍可能留在内核页缓存中;采用 Macrobenchmark 时应按前述固定版本的 COLD 协议记录清缓存结果。API 31+ 的平台页缓存清理入口不要求 root,不能把所有 cache drop 都归为 root 操作;线上启动分布还应覆盖真实缓存状态。

灰度发布中的 Profile 风险

Profile 是编译输入,覆盖不足会损失收益,覆盖过宽会增加编译时间和产物体积。分批放量(灰度)阶段需要同时看启动、安装/升级与稳定性。

Profile 与 APK 紧密绑定,同一版本里的运行时开关拿不掉已经打包或编译的 profile。需要线上对照时,应使用只改变 profile 的小流量受控版本或专项小版本,并在 Cloud Profile 尚未充分传播的发布早期按安装来源、安装时间和编译状态分组。代码、资源、服务端配置与用户入口必须保持一致;否则变化说不清是不是 profile 带来的。

风险触发条件可能表现核查方法
profile 与当前 DEX 不匹配复用旧版本产物、错误的 split、交付过程混入其他版本文件actual filter 退为 verify,Day-0 收益消失检查版本、DEX checksum、文件名与 -v 结果
规则覆盖过窄只录首页,遗漏登录态、deep link(直达特定页面的链接)、实验分支P50 改善,P90/P99 变化很小按入口、账号状态和配置分组
规则覆盖过宽把低频代码也纳入高频启动路径dex2oat 时间与产物体积增加监控安装耗时、dex2oat CPU 时间和 artifact size(编译产物大小)
R8/代码变化生成 profile 的产物与发布产物不一致profile 不匹配或有效规则减少由匹配当前源码与工具链的受控生成变体采集,再对最终 release 重写、打包并检查规则
热修复或自定义 ClassLoader启动代码转移到动态 DEXsecondary DEX 上出现类加载、解释执行和 JIT在 Perfetto 系统时间线中定位 DEX 来源与 JIT slice(时间片段)
动态特性模块on-demand(按需下载)split 尚未安装或没有对应 profile模块首次进入慢,base 启动正常单独检查每个 split 的安装与 dexopt 状态

profile 不匹配通常表现为编译优化缺失,本身不会直接改变 App 代码语义。不过,若同一次发布还修改了 split、热修复、R8 混淆或安装器流程,类加载失败和初始化异常也可能一起出现,所以分批放量看板至少要并排展示:

  • TTID、TTFD 与启动慢帧;
  • 安装/升级完成到首次可交互的耗时;
  • 崩溃率与 ANR 率;
  • 各 App 版本、渠道、Android 版本的 compiler filter 分布;
  • 安装失败与 profile mismatch 相关日志。

与 Baseline Profile 生成流程衔接

前半篇负责生成和维护 Baseline Profile;发布后,我们要逐层确认 ART 是否拿到、接受并使用了 profile。两条流程按下面的顺序衔接:

  1. 前半篇用匹配当前源码的受控生成变体采集规则,由工具链按最终 release 重写打包,再对 release 类产物用 Macrobenchmark 验证代表性场景。
  2. 检查 APK/AAB 条目、安装来源和每个 APK 各自对应的 .dm。
  3. 用 pm art dump 与 pm compile -v 读取 actual filter 和 compilation reason。
  4. 用 Perfetto 区分 JIT/类加载成本与业务初始化成本。
  5. 沿用 21.1 App 启动路径、监控与度量的度量方式,分版本、渠道和设备监控 TTID/TTFD。

ART 编译原理可回看 1.5,冷/温/热启动路径可回看 8.2。若问题已经定位到数据库、网络、锁或 SDK 同步任务,应治理对应任务,不要继续扩大 profile 规则。

厂商 ROM 编译策略差异

同一 APK 在不同设备上可能得到不同的编译结果。可变因素分三类:安装器是否交付 external metadata;系统 build、ART Mainline(可独立更新的 ART 系统模块)版本与 pm.dexopt.<reason>;dex2oat 并发数、允许使用的 CPU 核心集合、省电状态、温控和存储压力。

一轮跨 ROM 对比应保存以下材料:

  1. 设备 build fingerprint(唯一标识系统构建的字符串)、ART 模块版本、ABI 与安装来源;
  2. 安装后立即读取的 per-APK filter/reason;
  3. 同一自动化脚本采集的启动 trace;固定“启动三次”保证不了样本条件一致;
  4. idle/charging 维护窗口前后的 filter/reason;
  5. 设备温度、低电量与低存储状态;
  6. ROM 的 pm.dexopt 与 dex2oat 系统属性快照。

厂商对维护窗口和资源策略的修改没有统一公开清单。缺少源码或厂商文档时,应把差异标成设备观测;单台设备的行为,写不成 Android 17 规范。

动态特性模块与 Play 分发

App Bundle 的每个安装单元都要单独检查。Base APK 已按 speed-profile 编译,说明不了 on-demand feature(按需下载的功能模块)的 split 已安装、具有匹配 metadata 或完成 dexopt——这几件事要逐一确认。

场景需要检查的对象典型信号
安装后打开首页base APK、主 ABI主进程启动阶段的 filter、JIT 与类加载
首次进入按需模块新安装的 feature splitsplit 的 compiler filter、模块入口附近的 JIT 编译
升级后进入模块新版本 base 与 split旧 metadata 是否失效、升级首进是否回退
热修复后进入模块secondary/dynamic DEX自定义 ClassLoader、secondary DEX 与 profile 偏离

.dm 按 APK 文件名匹配,所以验证日志也要按 base/split 和 ABI 拆分。Play 对 Cloud Profile 及 metadata 的交付策略可能变化;若没有当前 Play 文档或实际取得的安装产物,我们能确认的只有设备侧怎样消费文件,服务端到底交付了什么要另找依据。

排查手册

我们把一次完整排查整理成七步:

  1. 固定 release 产物、设备 build、ART 模块和安装来源。
  2. 检查 APK/AAB 中的 Baseline Profile;有外部 .prof/.dm 时,按 base/split 核对文件名与版本。
  3. 安装后立即执行 pm art dump,记录每个 APK/ABI 的 filter 与 reason。
  4. 清本地 profile 后用 pm compile -m speed-profile -f -v 验证外部 profile,读取 actual filter。
  5. 用 pm compile --reset 建立无 AOT 编译组,再与目标 profile 组、设备稳态组比较。
  6. 在 Perfetto 中同时看 art::jit::*(ART 的 JIT 时间片段)、类加载、文件缺页和主线程任务。
  7. 分批放量数据按版本、渠道、Android 版本和设备分层,联合观察启动、安装/升级、崩溃与 ANR。

若 trace 的主要耗时来自业务初始化,profile 排查到这里应停止。扩大规则不会缩短任务自身耗时,还可能增加 dex2oat 与存储成本。

小结

Profile 优化要沿“场景生成 → release 打包 → 渠道交付 → ART 接受 → 实际编译状态 → 启动收益”逐层验证。Baseline/Cloud Profile 负责设备端编译输入,Startup Profile 负责构建期 DEX 布局,DM 只是可能承载外部输入的容器;任何一层存在文件,都替下一层证明不了已生效。最终结论仍要回到固定编译条件下的 TTID、TTFD、帧和功能验证。

参考资料