Now in Android 性能基准测试与 Baseline Profile 实战:深入解析 `:benchmarks` 模块
导读
本文以 Now in Android(NIA)仓库中的 :benchmarks 模块为核心,系统讲解如何在一个采用 Kotlin + Jetpack Compose 的多模块 Android 应用中建立"性能基准测试 + Baseline Profile 自动生成"的完整闭环。你将掌握 Macrobenchmark 的四种编译模式对比、启动耗时/帧率/功耗三类指标的采集方法、Baseline Profile 生成与 Dex Layout Optimization(Dex 布局优化)的落地方式,以及如何用 UiAutomator 封装可复用的用户旅程(Critical User Journey,CUJ)测试动作。仓库内所有源码证据均可在 benchmarks/ 目录下找到。
:benchmarks 模块的定位与依赖关系
在 settings.gradle.kts 中,:benchmarks 被显式纳入构建:include(":benchmarks")。它是整个仓库中唯一的 android-test 类型模块(benchmarks/build.gradle.kts 同时应用了 androidx.baselineprofile 与 nowinandroid.android.test 两个插件),承担两类职责:
- Macrobenchmark 基准测试:测量应用冷启动时间、列表滚动帧率、状态变更重组开销与功耗;
- Baseline Profile 生成:通过脚本化用户旅程,产出发布版 APK 内嵌的 Profile 规则,加速应用启动与关键路径执行。
benchmarks/README.md 给出的模块依赖图完整刻画了它在整个仓库拓扑中的位置。核心关系只有两条边:
:app -.->|baselineProfile| :benchmarks:应用模块通过baselineProfile(projects.benchmarks)(见 app/build.gradle.kts)引入 benchmarks 作为 Profile 生成源;:benchmarks -.->|testedApks| :app:基准测试模块通过targetProjectPath = ":app"指定被测应用,构成"测试模块 → 被测 APK"的配对关系。
这与图例中的语义一致:实线箭头(-->)表示编译期硬依赖(如 :core:data --> :core:database),虚线箭头(-.->)表示测试期/运行期依赖(如 :feature:foryou:impl -.-> :core:data)。完整的依赖图(mermaid 原文)如下:
---
config:
layout: elk
elk:
nodePlacementStrategy: SIMPLE
---
graph TB
subgraph :feature
direction TB
subgraph :feature:settings
direction TB
:feature:settings:impl[impl]:::android-library
end
subgraph :feature:foryou
direction TB
:feature:foryou:api[api]:::android-library
:feature:foryou:impl[impl]:::android-library
end
subgraph :feature:bookmarks
direction TB
:feature:bookmarks:api[api]:::android-library
:feature:bookmarks:impl[impl]:::android-library
end
subgraph :feature:search
direction TB
:feature:search:api[api]:::android-library
:feature:search:impl[impl]:::android-library
end
subgraph :feature:interests
direction TB
:feature:interests:api[api]:::android-library
:feature:interests:impl[impl]:::android-library
end
subgraph :feature:topic
direction TB
:feature:topic:api[api]:::android-library
:feature:topic:impl[impl]:::android-library
end
end
subgraph :sync
direction TB
:sync:work[work]:::android-library
end
subgraph :core
direction TB
:core:analytics[analytics]:::android-library
:core:common[common]:::jvm-library
:core:data[data]:::android-library
:core:database[database]:::android-library
:core:datastore[datastore]:::android-library
:core:datastore-proto[datastore-proto]:::jvm-library
:core:designsystem[designsystem]:::android-library
:core:domain[domain]:::android-library
:core:model[model]:::jvm-library
:core:navigation[navigation]:::android-library
:core:network[network]:::android-library
:core:notifications[notifications]:::android-library
:core:ui[ui]:::android-library
end
:benchmarks[benchmarks]:::android-test
:app[app]:::android-application
从图中可见:app 聚合了全部 feature(foryou/bookmarks/interests/search/topic/settings)与 core 能力模块;各 feature 的 api 分层依赖 :core:navigation,impl 分层依赖 :core:data、:core:designsystem、:core:ui 及各自的 api。这种 api/impl 双模块 + 显式依赖边约束,保证 Profile 生成与基准测试的对象(app)与线上发布产物保持一致。
构建配置:从 build.gradle.kts 看测试基础设施
:benchmarks/build.gradle.kts 的配置决定了测试的形态,几个关键点值得展开:
android {
namespace = "com.google.samples.apps.nowinandroid.benchmarks"
defaultConfig {
minSdk = 28
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "APP_BUILD_TYPE_SUFFIX", "\"\"")
}
buildFeatures { buildConfig = true }
// 与 app 保持一致的 flavor 维度,用于区分 demo/prod
configureFlavors(this) { flavor ->
buildConfigField("String", "APP_FLAVOR_SUFFIX",
"\"${flavor.applicationIdSuffix ?: ""}\"")
}
testOptions.managedDevices.localDevices {
create("pixel6Api33") {
device = "Pixel 6"; apiLevel = 33; systemImageSource = "aosp"
}
}
targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}
baselineProfile {
managedDevices.clear()
managedDevices += "pixel6Api33"
useConnectedDevices = false
}
- minSdk = 28:Macrobenchmark 要求被测设备 API 28 以上(冷启动与 Profile 采集的硬性前提)。
- self-instrumenting:启用后,测试模块与被测应用可处于同一进程运行,无需为
app注入独立测试包。 - managed device(GMD):固定使用
pixel6Api33(Pixel 6 / API 33 / aosp 镜像),并以useConnectedDevices = false强制走云端托管设备而非连接真机——注释明确说明这是为了"本地与 CI 构建的一致性"。 - flavor 同步:
:benchmarks与app共享同一套 flavor 维度,使得 Profile 既能在prod上生成(贴近线上真实数据),又能在demo上运行基准(使用稳定假数据,结果可复现)。
依赖声明集中在 gradle/libs.versions.toml 与 benchmarks/build.gradle.kts:androidx.benchmark.macro(Macrobenchmark API)、androidx.test.uiautomator(UI 自动化交互)、androidx.test.ext/rules/runner/espresso-core 等,插件侧使用 androidx.baselineprofile(版本引用 androidxMacroBenchmark,见 libs.versions.toml)。
应用侧集成:Profile 如何进入发布包
:app/build.gradle.kts 中的两处配置让 Profile 真正生效于发布构建:
release {
// 发布构建时强制重新生成 Baseline Profile,保证 Profile 与代码同步
baselineProfile.automaticGenerationDuringBuild = true
}
...
baselineProfile(projects.benchmarks) // 依赖 :benchmarks 作为生成源
baselineProfile {
// 常规 assemble 时不重复构建 Profile,仅 release 变体触发
automaticGenerationDuringBuild = false
// 通过 Startup Profile 启用 Dex 布局优化
dexLayoutOptimization = true
}
配合 implementation(libs.androidx.profileinstaller)(见 app/build.gradle.kts),Profile 会在应用安装后被 profileinstaller 读取并应用。dexLayoutOptimization = true 意味着生成 Profile 时会把高频启动类/方法的信息写入 Startup Profile,实现 Dex 布局优化——这正是 StartupBaselineProfile.kt 中 includeInStartupProfile = true 参数所对应的机制。
启动性能基准:四种编译模式与自定义指标
:StartupBenchmark.kt 是模块中最具代表性的基准:它以冷启动(StartupMode.COLD)为场景,用同一套用户旅程对比四种 CompilationMode,从而量化 Baseline Profile 的价值:
| 测试方法 | CompilationMode | 含义 |
|---|---|---|
startupWithoutPreCompilation |
CompilationMode.None() |
完全不预编译,纯 JIT,作为最差基线 |
startupWithPartialCompilationAndDisabledBaselineProfile |
Partial(baselineProfileMode = Disable, warmupIterations = 1) |
部分预编译但禁用 Profile,用于隔离 Profile 本身的收益 |
startupPrecompiledWithBaselineProfile |
Partial(baselineProfileMode = Require) |
应用现有 Baseline Profile(若缺失则失败),生产环境的真实形态 |
startupFullyPrecompiled |
CompilationMode.Full() |
全量 AOT 预编译,作为理论上限参照 |
measureRepeated 的关键参数:iterations = 20(注释说明"更多迭代带来更高统计显著性")、startupMode = COLD、setupBlock 中先 pressHome() 再授权通知,测量块内通过 startActivityAndAllowNotifications() 拉起应用并用 forYouWaitForContent() 等待首帧内容渲染完成,从而捕获 Time To Full Display(完全显示耗时)。
指标方面,BaselineProfileMetrics.kt 自定义了三个:
val jitCompilationMetric = TraceSectionMetric("JIT Compiling %", label = "JIT compilation")
val classInitMetric = TraceSectionMetric("L%/%;", label = "ClassInit")
val allMetrics = listOf(StartupTimingMetric(), jitCompilationMetric, classInitMetric)
StartupTimingMetric():标准启动耗时(冷启动/首帧等);JIT compilation:跟踪 JIT 编译耗时占比——"Profile 正确应用时该数值应下降";ClassInit:跟踪类初始化耗时——"Profile 正确应用时该数值应下降"。
两个 TraceSectionMetric 配合使用,可精确定位 Profile 未生效时的时间损耗发生在 JIT 编译还是类加载初始化阶段,为性能优化提供归因证据。
滚动帧率与重组基准:FrameTimingMetric 的应用
滚动性能是 Lazy 列表应用的典型瓶颈,模块用 FrameTimingMetric 覆盖三条用户旅程:
- For You 信息流:ScrollForYouFeedBenchmark.kt 在
WARM启动下,先forYouWaitForContent()等待数据加载、forYouSelectTopics()选中话题填充信息流,再forYouScrollFeedDownUp()完成一次下上往返滚动,iterations = 10。三个测试方法分别用CompilationMode.None/Partial/Full对照。 - Interests 话题列表:ScrollTopicListBenchmark.kt 导航到 Interests 页后,
repeat(3) { interestsScrollTopicsDownUp() }连续三次滚动。 - 重组开销:TopicsScreenRecompositionBenchmark.kt 在 Interests 页
repeat(3) { interestsToggleBookmarked() }反复切换话题书签状态,通过FrameTimingMetric度量状态变更引发的重组是否带来掉帧——这正是 Compose 下"不必要的重组"问题的最直接量化手段。
功耗基准:PowerMetric 与明暗主题对比
:ScrollTopicListPowerMetricsBenchmark.kt 展示了功耗测量的完整写法,且标注 @RequiresApi(VERSION_CODES.Q)(功耗测量需 Android 10+):
private val categories = PowerCategory.entries
.associateWith { PowerCategoryDisplayLevel.TOTAL }
metrics = listOf(FrameTimingMetric(), PowerMetric(PowerMetric.Energy(categories)))
iterations = 2
它同时注册 FrameTimingMetric 与 PowerMetric(Energy(...)),对所有 PowerCategory 以 TOTAL 显示级别采集能耗。两个测试分别以浅色(setAppTheme(false))与深色(setAppTheme(true))主题运行同一滚动旅程,用于对比不同主题下帧率与能耗的差异。setAppTheme 的实现位于 ForYouActions.kt:进入设置页点击 "Dark/Light" 并确认 "OK",再等待 niaTopAppBar 出现。
Baseline Profile 生成:四个脚本化用户旅程
benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/baselineprofile/ 下四个测试类分别覆盖应用四个核心场景,均通过 BaselineProfileRule().collect(PACKAGE_NAME) { ... } 录制执行路径生成 Profile:
| Profile 类 | 覆盖旅程 | 源码 |
|---|---|---|
StartupBaselineProfile |
冷启动(includeInStartupProfile = true,启用 Dex 布局优化) |
StartupBaselineProfile.kt |
ForYouBaselineProfile |
For You 页:等待加载 → 选话题 → 滚动信息流 | ForYouBaselineProfile.kt |
BookmarksBaselineProfile |
Bookmarks(Saved)页导航 | BookmarksBaselineProfile.kt |
InterestsBaselineProfile |
Interests 页导航 + 话题列表滚动 | InterestsBaselineProfile.kt |
以 ForYouBaselineProfile 为例,其 Profile 块复用了与滚动基准完全相同的动作序列(forYouWaitForContent → forYouSelectTopics(true) → forYouScrollFeedDownUp),体现了"基准测试与 Profile 生成共享同一套 CUJ 定义"的良好实践——Profile 覆盖的就是你最关心的性能路径。生成动作结束后,app 的 release 构建会把结果合并进 APK(baselineProfile.automaticGenerationDuringBuild = true),后续发布无需手工维护 Profile 文件。
UI 交互封装:可复用的动作库与辅助函数
模块将 UiAutomator 交互抽象为按功能域组织的扩展函数,供基准与 Profile 共用:
- 通用层(GeneralActions.kt):
startActivityAndWait()+ 通知授权包装为startActivityAndAllowNotifications();Android 13+(SDK_INT >= TIRAMISU)下通过device.executeShellCommand("pm grant $packageName android.permission.POST_NOTIFICATIONS")直接授权,避免点击系统弹窗的不确定性;getTopAppBar()按资源 idniaTopAppBar等待并返回应用顶栏。 - For You 域(ForYouActions.kt):
forYouWaitForContent()先等待loadingWheel消失,再等待forYou:topicSelection出现,并对其调用自定义条件untilHasChildren()最长 60 秒确保数据真正加载完成;forYouSelectTopics()遍历话题列表,通过By.checkable(true)判定选中态,对未选中项点击、对已选中项在recheckTopicsIfChecked时连点两次以保持基线一致。 - 滚动动作(Utils.kt):
flingElementDownUp(element)在setGestureMargin(displayWidth / 5)预留侧边距后执行fling(Direction.DOWN)与fling(Direction.UP)——注释指出这是"防止触发系统手势导航"。 - 自定义等待条件(UiAutomatorHelpers.kt):
untilHasChildren(count, op)支持AT_LEAST / EXACTLY / AT_MOST三种子节点数量断言,弥补 UiAutomator 内置条件的不足;文件特意放在androidx.test.uiautomator包内以访问包级私有方法。 - 包名处理(Utils.kt):
PACKAGE_NAME由com.google.samples.apps.nowinandroid+BuildConfig.APP_FLAVOR_SUFFIX拼接,确保 demo/prod flavor 下都命中正确的被测应用。
此外 BookmarksActions.kt 的 goToBookmarksScreen() 与 InterestsActions.kt 的 goToInterestsScreen()/interestsScrollTopicsDownUp()/interestsToggleBookmarked() 共同构成模块的 UI 操作矩阵。
运行方式与结果查看
基于构建配置(benchmarks/build.gradle.kts),可以推断出标准运行入口:
- 生成 Baseline Profile:执行
./gradlew :app:generateBaselineProfile(或对:benchmarks的对应 Profile 任务),Gradle 会在pixel6Api33托管设备上依次运行四个BaselineProfileRule测试并产出 Profile 资源; - 运行全部基准测试:执行
./gradlew :benchmarks:pixel6Api33BenchmarkAndroidTest(task 命名遵循"托管设备名 + BenchmarkAndroidTest"的 Gradle 规则),即可在托管设备上批量执行上述 Startup / Scroll / Recomposition / Power 基准; - 结果查看:如 StartupBenchmark.kt 的类注释所述,从 Android Studio 直接运行单个基准方法,即可查看启动耗时测量结果,并借助自动采集的系统 trace 分析冷启动阶段的性能瓶颈。
小结
:benchmarks 模块为 Now in Android 提供了"测量 — 归因 — 优化 — 固化"的完整性能工作流:通过四种 CompilationMode 的横向对比量化 Baseline Profile 收益,通过 StartupTimingMetric / FrameTimingMetric / PowerMetric / TraceSectionMetric 分别覆盖启动、滚动帧率、功耗与 JIT/类初始化归因,再以四个 BaselineProfileRule 用户旅程自动生成并内嵌 Profile 到发布包,最终配合 profileinstaller 与 Dex 布局优化提升真实用户体验。对任何多模块 Compose 应用而言,这套"基准与 Profile 共享 CUJ、GMD 保证结果一致、release 自动再生成"的工程模式都具备直接的迁移价值。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.24 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python670
SlideSCIPPT插件,支持素材库、AI助手、一键添加图片标题,复制粘贴位置、一键图片对齐、一键插入Markdown(加粗、超链接等行内样式、代码块、LaTeX等块级样式)、便捷导出图片!C#230
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python52874
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go22545
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java36351