调试基线配置文件

本文档展示了最佳实践,可帮助您诊断问题并确保基线配置文件正常工作以提供最大效益。

构建问题

如果您已在 Now in Android 示例应用中复制了基线配置文件示例,则在执行基线配置文件任务期间,您可能会遇到测试失败,提示无法在模拟器上运行测试

./gradlew assembleDemoRelease
Starting a Gradle Daemon (subsequent builds will be faster)
Calculating task graph as no configuration cache is available for tasks: assembleDemoRelease
Type-safe project accessors is an incubating feature.

> Task :benchmarks:pixel6Api33DemoNonMinifiedReleaseAndroidTest
Starting 14 tests on pixel6Api33

com.google.samples.apps.nowinandroid.foryou.ScrollForYouFeedBenchmark > scrollFeedCompilationNone[pixel6Api33] FAILED
        java.lang.AssertionError: ERRORS (not suppressed): EMULATOR
        WARNINGS (suppressed):
        ...

这些失败之所以发生,是因为 Now in Android 使用由 Gradle 管理的设备来生成基线配置文件。这些失败是预期的,因为通常不应在模拟器上运行性能基准测试。但是,由于您在生成基线配置文件时不会收集性能指标,因此为了方便起见,可以在模拟器上运行基线配置文件收集。要在模拟器上使用基线配置文件,请从命令行执行构建和安装,并设置参数以启用基线配置文件规则

installDemoRelease -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile

或者,您可以在 Android Studio 中通过选择 Run > Edit Configurations 来创建自定义运行配置,以在模拟器上启用基线配置文件

Add a custom run configuration to create Baseline Profiles in Now in Android
图 1. 在 Now in Android 中添加自定义运行配置以创建基线配置文件。

安装问题

检查您正在构建的 APK 或 AAB 是否来自包含基线配置文件的构建变体。最简单的检查方法是在 Android Studio 中通过选择 Build > Analyze APK 打开 APK,然后查找 /assets/dexopt/baseline.prof 文件中的配置文件

Check for a Baseline Profile using APK Viewer in Android Studio
图 2. 在 Android Studio 中使用 APK Viewer 检查基线配置文件。

基线配置文件需要在运行应用的设备上进行编译。对于应用商店安装和使用 PackageInstaller 安装的应用,设备上的编译作为应用安装过程的一部分进行。但是,当应用从 Android Studio 或使用命令行工具进行旁加载时,Jetpack ProfileInstaller 库负责在下一个后台 DEX 优化过程中将配置文件排队等待编译。在这些情况下,如果您想确保正在使用基线配置文件,您可能需要强制编译基线配置文件ProfileVerifier 允许您查询配置文件安装和编译的状态,如以下示例所示

Kotlin

private const val TAG = "MainActivity"

class MainActivity : ComponentActivity() {
  ...
  override fun onResume() {
    super.onResume()
    lifecycleScope.launch {
      logCompilationStatus()
    }
  }

  private suspend fun logCompilationStatus() {
     withContext(Dispatchers.IO) {
        val status = ProfileVerifier.getCompilationStatusAsync().await()
        when (status.profileInstallResultCode) {
            RESULT_CODE_NO_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Baseline Profile not found")
            RESULT_CODE_COMPILED_WITH_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Compiled with profile")
            RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
                Log.d(TAG, "ProfileInstaller: App was installed through Play store")
            RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST ->
                Log.d(TAG, "ProfileInstaller: PackageName not found")
            RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ ->
                Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read")
            RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE ->
                Log.d(TAG, "ProfileInstaller: Can't write cache file")
            RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            else ->
                Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued")
        }
    }
}

Java

public class MainActivity extends ComponentActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onResume() {
        super.onResume();

        logCompilationStatus();
    }

    private void logCompilationStatus() {
         ListeningExecutorService service = MoreExecutors.listeningDecorator(
                Executors.newSingleThreadExecutor());
        ListenableFuture<ProfileVerifier.CompilationStatus> future =
                ProfileVerifier.getCompilationStatusAsync();
        Futures.addCallback(future, new FutureCallback<>() {
            @Override
            public void onSuccess(CompilationStatus result) {
                int resultCode = result.getProfileInstallResultCode();
                if (resultCode == RESULT_CODE_NO_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Baseline Profile not found");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Compiled with profile");
                } else if (resultCode == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING) {
                    Log.d(TAG, "ProfileInstaller: App was installed through Play store");
                } else if (resultCode == RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST) {
                    Log.d(TAG, "ProfileInstaller: PackageName not found");
                } else if (resultCode == RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ) {
                    Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read");
                } else if (resultCode
                        == RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE) {
                    Log.d(TAG, "ProfileInstaller: Can't write cache file");
                } else if (resultCode == RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else {
                    Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued");
                }
            }

            @Override
            public void onFailure(Throwable t) {
                Log.d(TAG,
                        "ProfileInstaller: Error getting installation status: " + t.getMessage());
            }
        }, service);
    }
}

以下结果代码提供了某些问题的原因提示

RESULT_CODE_COMPILED_WITH_PROFILE
配置文件已安装、编译,并在应用运行时使用。这是您希望看到的结果。
RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED
正在运行的 APK 或 AAB 中未找到配置文件。如果看到此错误,请确保您正在使用包含基线配置文件的构建变体,并且 APK 包含配置文件。
RESULT_CODE_NO_PROFILE
通过应用商店或软件包管理器安装应用时,未为此应用安装配置文件。此错误代码的主要原因是 ProfileInstallerInitializer 被禁用,导致配置文件安装程序未运行。请注意,当报告此错误时,应用 APK 中仍找到了嵌入式配置文件。如果未找到嵌入式配置文件,则返回的错误代码为 RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED
RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
在 APK 或 AAB 中找到了配置文件,并已将其排队等待编译。当 ProfileInstaller 安装配置文件时,它会排队等待系统下一次运行后台 DEX 优化时进行编译。在编译完成之前,配置文件不会处于活动状态。在编译完成之前,请勿尝试对基线配置文件进行基准测试。您可能需要强制编译基线配置文件。当应用在运行 Android 9 (API 28) 及更高版本的设备上通过应用商店或软件包管理器安装时,不会发生此错误,因为编译是在安装期间执行的。
RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING
已安装不匹配的配置文件,并且应用已使用它进行编译。这是通过 Google Play 商店或软件包管理器安装的结果。请注意,此结果与 RESULT_CODE_COMPILED_WITH_PROFILE 不同,因为不匹配的配置文件将仅编译配置文件和应用之间仍共享的任何方法。该配置文件实际上比预期小,并且编译的方法将少于基线配置文件中包含的方法。
RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE
ProfileVerifier 无法写入验证结果缓存文件。这可能是由于应用文件夹权限出现问题,或者设备上没有足够的可用磁盘空间造成的。
RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
ProfileVerifier 在不受支持的 Android API 版本上运行。ProfileVerifier 仅支持 Android 9 (API 级别 28) 及更高版本。
RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST
查询应用软件包的 PackageManager.NameNotFoundException 时会抛出 PackageManager 异常。这种情况应该很少发生。尝试卸载应用并重新安装所有内容。
RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ
存在以前的验证结果缓存文件,但无法读取。这种情况应该很少发生。尝试卸载应用并重新安装所有内容。

在生产环境中使用 ProfileVerifier

在生产环境中,您可以将 ProfileVerifier 与分析报告库(例如 Google Analytics for Firebase)结合使用,以生成指示配置文件状态的分析事件。例如,如果发布了不包含基线配置文件的新应用版本,这会快速提醒您。

强制编译基线配置文件

如果您的基线配置文件的编译状态为 RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION,则可以使用 adb 强制立即编译

adb shell cmd package compile -r bg-dexopt PACKAGE_NAME

不使用 ProfileVerifier 检查编译状态

如果您不使用 ProfileVerifier,则可以使用 adb 检查编译状态,尽管它不像 ProfileVerifier 那样提供深入的洞察。

adb shell dumpsys package dexopt | grep -A 2 PACKAGE_NAME

使用 adb 会生成类似于以下内容的结果

  [com.google.samples.apps.nowinandroid.demo]
    path: /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/base.apk
      arm64: [status=speed-profile] [reason=bg-dexopt] [primary-abi]
        [location is /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/oat/arm64/base.odex]

status 值表示配置文件编译状态,是以下值之一

编译状态 含义
speed‑profile 已存在已编译的配置文件并正在使用。
verify 不存在已编译的配置文件。

处于 verify 状态并不意味着 APK 或 AAB 不包含配置文件,因为它可能已排队等待下一个后台 DEX 优化任务进行编译。

reason 值表示触发配置文件编译的原因,是以下值之一

原因 含义
install‑dm 应用安装时,基线配置文件由手动或 Google Play 编译。
bg‑dexopt 在设备闲置时编译了配置文件。这可能是基线配置文件,也可能是应用使用期间收集的配置文件。
cmdline 编译是使用 adb 触发的。这可能是基线配置文件,也可能是应用使用期间收集的配置文件。

性能问题

本节介绍了一些正确定义和基准测试基线配置文件的最佳实践,以便从中获得最大收益。

正确基准测试启动指标

如果您的启动指标定义明确,您的基线配置文件将更有效。两个关键指标是首次显示时间 (TTID)完全显示时间 (TTFD)

TTID 是指应用绘制其第一帧的时间。将其保持尽可能短的时间非常重要,因为显示内容会向用户表明应用正在运行。您甚至可以显示不确定进度指示器,以表明应用正在响应。

TTFD 是指应用可以实际进行交互的时间。将其保持尽可能短的时间非常重要,以避免用户沮丧。如果您正确地发出 TTFD 信号,则表示您正在告诉系统,在达到 TTFD 过程中运行的代码是应用启动的一部分。因此,系统更有可能将此代码放入配置文件中。

尽可能降低 TTID 和 TTFD,以使您的应用感觉响应迅速。

系统能够检测 TTID,在 Logcat 中显示它,并将其作为启动基准测试的一部分进行报告。但是,系统无法确定 TTFD,应用有责任在达到完全绘制的交互状态时进行报告。您可以通过调用 reportFullyDrawn() 或(如果您使用 Jetpack Compose)ReportDrawn 来完成此操作。如果您有多个后台任务都需要在应用被视为完全绘制之前完成,那么您可以使用 FullyDrawnReporter,如提高启动时间准确性中所述。

库配置文件和自定义配置文件

在对配置文件影响进行基准测试时,可能难以将您应用配置文件的优势与库(例如 Jetpack 库)贡献的配置文件区分开来。当您构建 APK 时,Android Gradle 插件会添加库依赖项中的任何配置文件以及您的自定义配置文件。这对于优化整体性能很有好处,并且建议用于您的发布版本。但是,这使得很难衡量您的自定义配置文件带来了多少额外的性能提升。

手动查看自定义配置文件提供的额外优化的一种快速方法是将其删除并运行基准测试。然后替换它并再次运行基准测试。比较两者将向您展示仅由库配置文件提供的优化,以及库配置文件加上您的自定义配置文件提供的优化。

比较配置文件的一种可自动化方法是创建新的构建变体,其中仅包含库配置文件而不包含您的自定义配置文件。将此变体的基准测试与包含库配置文件和自定义配置文件的发布变体进行比较。以下示例显示了如何设置仅包含库配置文件的变体。将名为 releaseWithoutCustomProfile 的新变体添加到您的配置文件使用者模块(通常是您的应用模块)

Kotlin

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    create("releaseWithoutCustomProfile") {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile(project(":baselineprofile"))
}

baselineProfile {
  variants {
    create("release") {
      from(project(":baselineprofile"))
    }
  }
}

Groovy

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    releaseWithoutCustomProfile {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile ':baselineprofile"'
}

baselineProfile {
  variants {
    release {
      from(project(":baselineprofile"))
    }
  }
}

前面的代码示例从所有变体中移除了 baselineProfile 依赖项,并将其选择性地仅应用于 release 变体。当移除了对配置文件生成器模块的依赖项时,库配置文件仍在添加,这可能看起来有些反直觉。但是,此模块仅负责生成您的自定义配置文件。Android Gradle 插件仍对所有变体运行,并负责包含库配置文件。

您还需要将新变体添加到配置文件生成器模块。在此示例中,生成器模块名为 :baselineprofile

Kotlin

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      create("releaseWithoutCustomProfile") {}
      ...
    }
  ...
}

Groovy

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      releaseWithoutCustomProfile {}
      ...
    }
  ...
}

从 Android Studio 运行基准测试时,选择 releaseWithoutCustomProfile 变体以仅使用库配置文件衡量性能,或者选择 release 变体以使用库和自定义配置文件衡量性能。

避免 I/O 密集型应用启动

如果您的应用在启动期间执行大量 I/O 调用或网络调用,这可能会对应用启动时间和启动基准测试的准确性产生负面影响。这些耗时调用可能需要不确定的时间,这些时间会随时间变化,甚至在同一基准测试的不同迭代之间也会有所不同。I/O 调用通常优于网络调用,因为后者可能会受到设备外部因素和设备本身因素的影响。避免在启动期间进行网络调用。如果无法避免使用其中一种,请使用 I/O。

我们建议您的应用架构支持在没有网络或 I/O 调用的情况下启动应用,即使仅在基准测试启动时使用它。这有助于确保您的基准测试不同迭代之间的可变性尽可能小。

如果您的应用使用 Hilt,您可以在 微基准测试和 Hilt 中进行基准测试时提供伪造的 I/O 密集型实现。

覆盖所有重要的用户历程

在生成基线配置文件时准确覆盖所有重要的用户历程非常重要。任何未覆盖的用户历程都不会通过基线配置文件得到改进。最有效的基线配置文件包括所有常见的启动用户历程以及对性能敏感的应用内用户历程,例如滚动列表。