基准配置文件

基准配置文件是一个列表,其中包含在应用安装期间 Android 运行时 (ART) 将关键路径预编译为机器代码时使用的 APK 中包含的类和方法。这是一种由配置文件引导的优化 (PGO),可让应用优化启动、减少卡顿,并提升性能,从而让最终用户获享更好的使用体验。

基准配置文件的运作方式

配置文件规则会在 APK 的 assets/dexopt/baseline.prof 中编译为二进制文件形式。

在应用安装期间,ART 会对配置文件中的方法执行预先 (AOT) 编译,以便提升这些方法的执行速度。如果配置文件包含应用启动或帧渲染期间使用的方法,用户将获享启动速度更快且/或卡顿更少的体验。

在开发应用或库时,请考虑定义基准配置文件,以涵盖关键用户历程期间渲染时间或延迟非常重要的特定热路径,例如启动、转换或滚动。

接着,基准配置文件会与 APK 一起直接发送给用户(通过 Google Play),如图 1 所示:

图 1. 此图显示了从上传到向最终用户交付的基准配置文件工作流,以及该工作流与云配置文件的关系。

使用基准配置文件的原因

若要提升应用的用户互动度,缩短启动时间是一个至关重要的部分。提升应用的速度和响应能力有助于提高日活跃用户数和平均回访率。

云配置文件也可优化这些互动,但仅在更新发布后的一天(或更长时间)内可供用户使用,并且不支持 Android 7 (API 24) 到 Android 8 (API 26)。

各种 Android 版本的编译行为

Android 平台版本使用了不同的应用编译方法,每种方法都有相应的性能权衡。基准配置文件提供了一个适用于所有安装的配置文件,对之前的编译方法进行了改进。

Android 版本 编译方法 优化方法
Android 5(API 级别 21)到 Android 6(API 级别 23) 完全 AOT 整个应用会在安装期间进行优化,这会导致用户需要等待较长的时间才能使用应用,RAM 和磁盘空间使用量增加,并且从磁盘加载代码需要更长的时间,进而可能增加冷启动时间。
Android 7(API 级别 24)到 Android 8.1(API 级别 27) 部分 AOT(基准配置文件) 基准配置文件由 androidx.profileinstaller 在应用首次运行时安装,届时应用模块会定义此依赖项。ART 可以通过以下方法改进这一点:在应用使用期间添加额外的配置文件规则,并在设备空闲时编译这些规则。这可以进行相应优化,以便更好地利用磁盘空间并缩短从磁盘加载代码所需的时间,从而减少用户使用应用需要等待的时间。
Android 9(API 级别 28)及更高版本 部分 AOT(基准配置文件 + 云配置文件) 在应用安装期间,Play 会使用基准配置文件优化 APK 和云配置文件(如果有)。应用安装后,ART 配置文件会上传到 Play 并汇总在一起,然后在其他用户安装/更新应用时,以云配置文件的形式提供给他们。

创建基准配置文件

使用 BaselineProfileRule 自动创建配置文件规则

作为应用开发者,您可以使用 Jetpack Macrobenchmark 库为每个应用版本自动生成配置文件。

如需使用 Macrobenchmark 库创建基准配置文件,请执行以下操作:

  1. 在应用的 build.gradle 中为 ProfileInstaller 库添加一个依赖项,以启用本地和 Play 商店中的基准配置文件编译。这是在本地旁加载基准配置文件的唯一方式。

    dependencies {
         implementation("androidx.profileinstaller:profileinstaller:1.2.0-beta01")
    }
    
  2. 在 Gradle 项目中设置一个 Macrobenchmark 模块

  3. 定义一个称为 BaselineProfileGenerator 的新测试,如下所示:

    @ExperimentalBaselineProfilesApi
    @RunWith(AndroidJUnit4::class)
    class BaselineProfileGenerator {
        @get:Rule val baselineProfileRule = BaselineProfileRule()
    
        @Test
        fun startup() =
            baselineProfileRule.collectBaselineProfile(packageName = "com.example.app") {
                pressHome()
                // This block defines the app's critical user journey. Here we are interested in
                // optimizing for app startup. But you can also navigate and scroll
                // through your most important UI.
                startActivityAndWait()
            }
    }
    
  4. 连接到搭载 Android 9 或更高版本的 userdebug,或连接到搭载 Android 9 或更高版本且已启用 root 权限的 Android 开源项目 (AOSP) 模拟器。

  5. 从终端运行 adb root 命令,以确保 adb 守护程序以 root 权限运行。

  6. 运行测试并等待测试完成。

  7. 在 Logcat 中找到生成的配置文件所在的位置。搜索日志标签 Benchmark

    com.example.app D/Benchmark: Usable output directory: /storage/emulated/0/Android/media/com.example.app
    
    # List the output baseline profile
    ls /storage/emulated/0/Android/media/com.example.app
    SampleStartupBenchmark_startup-baseline-prof.txt
    
  8. 从设备拉取生成的文件。

    adb pull storage/emulated/0/Android/media/com.example.app/SampleStartupBenchmark_startup-baseline-prof.txt .
    
  9. 将生成的文件重命名为 baseline-prof.txt,并将其复制到应用模块的 src/main 目录。

手动定义配置文件规则

您可以在应用中或在库模块中手动定义配置文件规则,方法是在 src/main 目录中创建一个名为 baseline-prof.txt 的文件。这是包含 AndroidManifest.xml 文件的同一个文件夹。

该文件中的每行指定了一条规则。每条规则均表示一种模式,用于匹配应用或库中需要优化的方法或类。

这些规则的语法是使用 adb shell profman --dump-classes-and-methods 时所用的易于用户理解的 ART 配置文件格式 (HRF) 的超集。该语法与描述符和签名的语法非常相似,但还允许使用通配符以简化规则编写过程。

以下示例展示了 Jetpack Compose 库中包含的一些基准配置文件规则:

HSPLandroidx/compose/runtime/ComposerImpl;->updateValue(Ljava/lang/Object;)V
HSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)I
HLandroidx/compose/runtime/ComposerImpl;->validateNodeExpected()V
PLandroidx/compose/runtime/CompositionImpl;->applyChanges()V
HLandroidx/compose/runtime/ComposerKt;->findLocation(Ljava/util/List;I)I
Landroidx/compose/runtime/ComposerImpl;

规则语法

这些规则采用两种形式,一种用于方法,一种用于类:

[FLAGS][CLASS_DESCRIPTOR]->[METHOD_SIGNATURE]

类规则使用以下格式:

[CLASS_DESCRIPTOR]
语法 说明
FLAGS 表示 HSP 中的一个或多个字符,用于指示相应方法在启动类型方面应标记为 HotStartup 还是 Post Startup

带有 H 标记表示相应方法是一种“热”方法,这意味着相应方法在应用的整个生命周期内会被调用多次。

带有 S 标记表示相应方法在启动时被调用。

带有 P 标记表示相应方法是与启动无关的热方法。

如果某个类出现在此文件中,则表示系统会在启动过程中使用该类,并且应在堆中预先分配该类,以避免耗费资源来加载它。ART 编译器会采用各种优化策略,例如:对这些方法进行 AOT 编译,以及在生成的 AOT 文件中执行布局优化。
CLASS_DESCRIPTOR 目标方法的类的描述符。例如,androidx.compose.runtime.SlotTable 应具有描述符 Landroidx/compose/runtime/SlotTable;。注意:根据 Dalvik 可执行文件 (DEX) 格式,描述符中附加了 L。
METHOD_SIGNATURE 方法的签名,包括方法的名称、参数类型和返回值类型。例如,LayoutNode 上的方法

// LayoutNode.kt

fun isPlaced():Boolean {
// ...
}

具有签名 isPlaced()Z

这些格式可以包含通配符,以便让单个规则囊括多个方法或类。如需获取在 Android Studio 中使用规则语法编写代码方面的指导帮助,请参阅 Android 基准配置文件插件。

下面显示了一个通配符规则示例:

HSPLandroidx/compose/ui/layout/**->**(**)**

基准配置文件规则支持的类型

基准配置文件规则支持以下类型。如需详细了解这些类型,请参阅 Dalvik 可执行文件 (DEX) 格式

字符 类型 说明
B byte 有符号字节
C char 采用 UTF-16 编码的 Unicode 字符码位
D double 双精度浮点值
F float 单精度浮点值
I int 整数
J long 长整数
S short 有符号短数值
V void Void
Z boolean true 或 false
L(类名称) reference 类名称的实例

此外,库还可以定义将打包到 AAR 工件中的规则。当您构建 APK 来包含这些工件时,相应规则会合并在一起(类似于清单的合并方式),并编译成 APK 专用的紧凑型二进制 ART 配置文件。

对于搭载 Android 9(API 级别 28)或 Android 7(API 级别 24)并且使用 ProfileInstaller 的设备,当在设备上使用 APK 时,ART 会利用此配置文件在应用安装时对应用的特定子集进行 AOT 编译。

其他说明

创建基准配置文件时,还需要注意一些其他事项:

  • Android 5 到 Android 6(API 级别 21 和 23)已在安装时对 APK 进行 AOT 编译。

  • 可调试应用绝不会经过 AOT 编译,以帮助进行问题排查。

  • 规则文件必须命名为 baseline-prof.txt,并放在主源代码集的根目录中(它应该是 AndroidManifest.xml 文件的同级文件)。

  • 只有在您使用 Android Gradle 插件 7.1.0-alpha05 或更高版本 (Android Studio Bumblebee Canary 5) 时,才会用到这些文件。

  • Bazel 目前不支持读取基准配置文件,也不支持将其合并到 APK 中。

  • 基准配置文件的压缩后大小不得超过 1.5 MB。因此,库和应用应尽量定义一小部分能够最大限度提升影响的配置文件规则。

  • 如果宽泛规则对应用编译过多,就会增加磁盘访问,进而降低启动速度。您应测试基准配置文件的性能。

衡量改进

使用 Macrobenchmark 库自动进行衡量

借助 Macrobenchmark,您可以通过 CompilationMode API 控制预衡量编译,包括对 BaselineProfile 的使用。

如果您已在 Macrobenchmark 模块中设置 BaselineProfileRule 测试,则可以在该模块中定义一项新测试来评估其性能:

@RunWith(AndroidJUnit4::class)
class BaselineProfileBenchmark {
  @get:Rule
  val benchmarkRule = MacrobenchmarkRule()

  @Test
  fun startupNoCompilation() {
    startup(CompilationMode.None())
  }

  @Test
  fun startupBaselineProfile() {
    startup(CompilationMode.Partial(
      baselineProfileMode = BaselineProfileMode.Require
    ))
  }

  private fun startup(compilationMode: CompilationMode) {
    benchmarkRule.measureRepeated(
      packageName = "com.example.app",
      metrics = listOf(StartupTimingMetric()),
      iterations = 10,
      startupMode = StartupMode.COLD,
      compilationMode = compilationMode
    ) { // this = MacrobenchmarkScope
        pressHome()
        startActivityAndWait()
    }
  }
}

图 2 中显示了一个“通过”测试结果的示例:

图 2. 这些是一个小型测试的结果。更大的应用可以更大程度地受益于基准配置文件。

请注意,虽然上述示例关注的是 StartupTimingMetric,但还有一些其他重要指标值得考虑,例如卡顿(帧指标),您可以使用 Jetpack Macrobenchmark 衡量该指标。

手动衡量应用改进

首先,我们衡量一下未优化的应用启动,以便作为参考。

PACKAGE_NAME=com.example.app
# Force Stop App
adb shell am force-stop $PACKAGE_NAME
# Reset compiled state
adb shell cmd package compile --reset $PACKAGE_NAME
# Measure App startup
# This corresponds to `Time to initial display` metric
# For additional info https://developer.android.com/topic/performance/vitals/launch-time#time-initial
adb shell am start-activity -W -n $PACKAGE_NAME/.ExampleActivity \
 | grep "TotalTime"

接下来,我们要旁加载基准配置文件。

# Unzip the Release APK first
unzip release.apk
# Create a ZIP archive
# Note: The name should match the name of the APK
# Note: Copy baseline.prof{m} and rename it to primary.prof{m}
cp assets/dexopt/baseline.prof primary.prof
cp assets/dexopt/baseline.profm primary.profm
# Create an archive
zip -r release.dm primary.prof primary.profm
# Confirm that release.dm only contains the two profile files:
unzip -l release.dm
# Archive:  release.dm
#   Length      Date    Time    Name
# ---------  ---------- -----   ----
#      3885  1980-12-31 17:01   primary.prof
#      1024  1980-12-31 17:01   primary.profm
# ---------                     -------
#                               2 files
# Install APK + Profile together
adb install-multiple release.apk release.dm

如需验证软件包在安装时是否已优化,请运行以下命令:

# Check dexopt state
adb shell dumpsys package dexopt | grep -A 1 $PACKAGE_NAME

输出应指明已编译软件包。

[com.example.app]
  path: /data/app/~~YvNxUxuP2e5xA6EGtM5i9A==/com.example.app-zQ0tkJN8tDrEZXTlrDUSBg==/base.apk
  arm64: [status=speed-profile] [reason=install-dm]

现在,我们可以像前面那样衡量应用启动性能,但不重置编译状态。

# Force Stop App
adb shell am force-stop $PACKAGE_NAME
# Measure App startup
adb shell am start-activity -W -n $PACKAGE_NAME/.ExampleActivity \
 | grep "TotalTime"

已知问题

目前,使用基准配置文件存在几个已知问题:

  • 从 app bundle 构建 APK 时,未正确封装基准配置文件。如需解决此问题,请采用 com.android.tools.build:gradle:7.3.0-beta02 及更高版本(详见问题)。

  • 只针对主 classes.dex 文件正确封装了基准配置文件。这会影响包含多个 .dex 文件的应用。如需解决此问题,请采用 com.android.tools.build:gradle:7.3.0-beta02 及更高版本(详见问题)。

  • Macrobenchmark 与 Android 12L (API 32)(详见问题)和 Android 13 (API 33)(详见问题)上的基准配置文件不兼容。

  • 不允许在 user(未取得 root 权限)build 中重置 ART 配置文件缓存。为了解决此问题,androidx.benchmark:benchmark-macro-junit4:1.1.0-rc02 包含可在基准测试期间重新安装应用的修复程序(详见问题)。

  • 在对应用进行性能分析时,Android Studio 性能分析器不会安装基准配置文件(详见问题)。

  • 非 Gradle 构建系统(Bazel、Buck 等)目前不支持将基准配置文件编译到输出 APK 中。

  • 目前,Play 商店在 AAB 文件上传后需要 10-14 小时才能使基准配置文件可供安装。在此期间下载应用的用户无法享受这些好处,直到后台 dexopt 运行(可能一夜过后)。我们正在积极进行改进。

  • 非 Play 商店应用分发渠道可能不支持在安装时使用基准配置文件。通过这些渠道安装应用的用户无法享受这些好处,直到后台 dexopt 运行(可能一夜过后)。