揭密:Compose应用如何做到启动提升34%

本文译自「How I Found a 34% Startup Win in a Modern Compose App」,原文链接levelup.gitconnected.com/how-i-found...,由James Cullimore发布于2026年3月16日。

人们很容易认为,一旦 Android 应用变得现代化、使用 Compose、结构合理且运行流畅,那么大幅提升启动速度的潜力就已经发挥出来了。我最初也是这么想的。

但事实证明我错了。

我当时正在开发一款现代化的 Compose 应用,它有两种截然不同的启动路径:初始设置流程和直接启动到主操作界面。这款应用本身已经具备很多优势:支持基线配置文件、启动基准测试、跟踪功能,以及代码库也并非一团糟。

然而,我仍然有 34% 的启动速度提升空间。

有趣的是,最终的成果并非来自单一的灵丹妙药。我添加了重组检测功能,使 UI 更易于理解,然后又通过 StrictMode 模式优化了启动和设置行为。基线性能分析很有帮助。宏基准测试也帮我验证了这一点。但最大的绝对收益来自于一些并非一开始就以"性能优化"为目的的清理工作。

这也不是我第一次做这类工作。我曾在四个不同的应用程序上进行过类似的启动和基线性能分析调查。这一点很重要,因为人们很容易把性能工具的使用误认为,只要添加了推荐的组件,结果就总是可以预测的。事实并非如此。在其中一个应用程序中,基线性能分析实际上反而减慢了启动速度。因此,虽然本文以一个项目为例,但结论来自对不同代码库的反复尝试,而不是某个偶然的结果。

简而言之:

  • 基线性能分析的效果清晰且可重复。

  • 宏基准测试很重要,因为启动过程并非单一流程。

  • 重构插桩比原始启动数据更能提高可见性。

  • 后续由严格模式和产品完整性检查触发的清理操作,最终使启动速度提升了 18% 到 34%,尽管它最初并非一项启动优化任务。

启动故事分阶段展开

基于日期的图表之所以会产生误导,其中一个原因是它们将工作简化为日历上的点,而不是工程阶段。

实际情况更接近于以下阶段:

  • 第一阶段:验证基准配置是否对当前应用的行为模式有实质性的帮助

  • 第二阶段:在严格模式的指导下进行清理和产品强化,然后再次进行基准测试,以查看启动过程的绝对变化

这种区别至关重要,因为后期的收益并非"更多的基准配置工作"。基准配置仍然有效,但更大的绝对变化来自于对启动路径周围行为的清理。

因此,当我在本文中比较两个基准测试快照时,我实际上并非在比较日期。我正在比较两个技术阶段:

  • 阶段 1:在之前的版本树上验证了基线配置文件的优势

  • 阶段 2:基线配置文件仍然存在,但现在是在严格模式/产品强化阶段的启动清理之上

基线配置文件是最容易预测的优势

最容易论证的是基线配置文件的影响,因为测量设置使比较变得明确。

基准测试脚本在一次运行中测量应用程序的两种编译模式:

  • before:使用 verify 编译的包

  • after:使用 speed-profile 编译的包

这意味着每次运行都会告诉我基线配置文件在当前版本树上的作用,而不仅仅是启动是否由于某些无关原因而发生变化。

bash 复制代码
#!/usr/bin/env bash
set -euo pipefail

PKG="dev.jamescullimore.app"
ACTIVITY="dev.jamescullimore.app/.MainActivity"
ITERATIONS=16
INSTALL_APP=0
ONLY_SCENARIO=""

collect_times() {
  local scenario="$1"
  local mode_label="$2"
  local onboarding="$3"
  local compile_mode="$4"

  adb shell am force-stop "$PKG" >/dev/null
  adb shell pm clear "$PKG" >/dev/null
  adb shell cmd package compile --reset "$PKG" >/dev/null
  adb shell cmd package compile -f -m "$compile_mode" "$PKG" >/dev/null

  adb shell am start -S -W \
    -a android.intent.action.MAIN \
    -c android.intent.category.LAUNCHER \
    -n "$ACTIVITY" \
    --ez perf_onboarding_done "$onboarding"
}

在"阶段 1",主要近期变更在于基线配置文件验证和刷新,基准测试结果显示:

在"阶段 2",经过严格模式引导的清理和产品强化后,基准测试结果仍然显示出显著的基线配置文件效果:

这种一致性至关重要。基线配置文件并非一次性优势,不会随着应用程序的变更而消失。即使在启动路径的其他部分得到改进之后,它仍然有效。

这也阐明了两个阶段之间的变化。基线配置文件的效果基本保持不变。绝对启动时间有所改善,是因为应用程序本身在首帧加载和早期交互方面变得更加流畅。

此外,必须明确指出一个令人不安的事实:基准配置文件并非万能,生成它并不意味着就能自动提升性能。在我进行过类似工作的四个应用程序中,有一个启用基准配置文件后启动速度反而变慢了。正因如此,我才如此重视在实际应用程序中测量优化前后的性能,而不是将基准配置文件视为一种盲目添加的工具。

这也是为什么我喜欢展示基准测试流程图,而不仅仅是最终的图表。该脚本足够简单,便于审核。它会强制停止应用程序,清除包状态,使用特定模式进行编译,使用 am start -S -W 命令启动,并在汇总中位数之前存储原始的 TotalTime 样本。

bash 复制代码
adb shell pm clear "$PKG" >/dev/null
adb shell cmd package compile - reset "$PKG" >/dev/null
adb shell cmd package compile -f -m "$compile_mode" "$PKG" >/dev/null

虽然这并不华丽,但它让整个过程更加务实。

启动并非单一流程,因此基准测试也并非单一测试

许多启动工作都被简化为单一的启动器指标。但这对于这款应用来说并不可靠。

有两个重要的启动路径:

  • setup_flow,此时用户引导尚未完成

  • main_flow,此时应用直接启动到主屏幕

基准测试模块明确地测量了这两种路径:

kotlin 复制代码
package dev.jamescullimore.app.benchmark

import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import org.junit.Rule
import org.junit.Test

class StartupBenchmarks {

    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun coldStartupWithoutProfile_setupFlow() = measureColdStart(
        compilationMode = CompilationMode.None(),
        onboardingDone = false
    )

    @Test
    fun coldStartupWithBaselineProfile_mainFlow() = measureColdStart(
        compilationMode = CompilationMode.Partial(),
        onboardingDone = true
    )

    private fun measureColdStart(
        compilationMode: CompilationMode,
        onboardingDone: Boolean
    ) = benchmarkRule.measureRepeated(
        packageName = "dev.jamescullimore.app",
        metrics = listOf(StartupTimingMetric()),
        startupMode = StartupMode.COLD,
        compilationMode = compilationMode,
        iterations = 10
    ) {
        launchForStartup(onboardingDone = onboardingDone)
    }
}

这是一个小细节,但它改变了我对启动工作的看法。如果我只测量了正常启动路径的情况,我就会忽略这样一个事实:设置启动和直接启动都值得追踪,而且它们对更改的响应略有不同。

这也使得最终的基准测试数据更具说服力。当两种情况都得到改善时,结果就很难被简单地归咎于路径特定的偶然因素。

标准工具有所帮助,但并非完全可靠

工作中有一部分比我预想的要棘手得多:基线配置文件的生成本身。

在这个设备上,常用的 BaselineProfileRule 路径无法稳定返回结果。我升级了 AndroidX 基准测试组件,减少了空闲等待时间,并更严格地隔离了生成测试。但运行仍然卡在"0/1 已完成"的状态。

这很可能导致团队耸耸肩,对已提交的配置文件置之不理,然后继续讨论基线配置文件,仿佛工作流程已经稳定。我不想看到这种情况发生。

因此,项目最终采用了手动回退生成器,它会启动相关场景,触发 androidx.profileinstaller.action.SAVE_PROFILE,导出 ART 配置文件,并将其过滤到 baseline-prof.txt 文件中。

bash 复制代码
#!/usr/bin/env bash
set -euo pipefail

PKG="dev.jamescullimore.app"
ACTIVITY="dev.jamescullimore.app/.MainActivity"
RECEIVER="dev.jamescullimore.app/androidx.profileinstaller.ProfileInstallReceiver"
REMOTE_PROFILE="/data/misc/profman/${PKG}-primary.prof.txt"

run_scenario() {
  local onboarding="$1"
  adb shell am start -S -W -n "$ACTIVITY" --ez perf_onboarding_done "$onboarding" >/dev/null
  sleep 5
  adb shell am broadcast -a androidx.profileinstaller.action.SAVE_PROFILE "$RECEIVER" >/dev/null
}

run_scenario false
run_scenario true

adb shell pm dump-profiles --dump-classes-and-methods "$PKG" >/dev/null
adb exec-out cat "$REMOTE_PROFILE" >/tmp/app-baseline-raw.prof.txt
python3 scripts/filter_baseline_profile.py /tmp/app-baseline-raw.prof.txt app/src/main/baseline-prof.txt

我不会把它描述为理想的工作流程,但我会把它描述为最真实的工作流程。官方工具仍然是首选,但如果它在你的环境中不稳定,你需要一个能够保持信心的备用方案,而不是回避问题。

这也是性能优化中不可或缺的一部分。有时,难点不在于找到优化点,而在于创建一个足够可靠、可以持续使用的工作流程。

重组检测更多的是为了提高可见性而非直接提升启动速度

我还希望更好地了解主 Compose 界面启动后正在执行的操作。这促使我在主屏幕上添加了一个仅供调试使用的小型重构检测叠加层。

kotlin 复制代码
@Composable
private fun RecomposeDebugOverlay(modifier: Modifier = Modifier) {
    if (!BuildConfig.DEBUG) return

    val counts by RecomposeDebugTracker.counts.collectAsStateWithLifecycle()
    var expanded by rememberSaveable { mutableStateOf(false) }

    Surface(modifier = modifier.clickable { expanded = !expanded }) {
        Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) {
            Text(text = "Recompose ${if (expanded) "hide" else "show"}")
            if (expanded) {
                counts.toList().sortedBy { it.first }.forEach { (name, count) ->
                    Text(text = "$name: $count")
                }
                TextButton(onClick = { RecomposeDebugTracker.reset() }) {
                    Text(text = stringResource(id = R.string.clear))
                }
            }
        }
    }
}

@Composable
private fun DebugRecomposeCounter(section: String) {
    if (!BuildConfig.DEBUG) return
    SideEffect {
        val count = RecomposeDebugTracker.increment(section)
        if (count % 25 == 0) {
            Log.d("Recompose", "$section recomposed $count times")
        }
    }
}

该叠加层会跟踪节级别的重构操作,并在 UI 中显示出来:

kotlin 复制代码
@Composable
private fun DebugRecomposeCounter(section: String) {
   ...
}

由于仅供调试使用的工具在长期未使用后容易失效,因此我们还编写了一个 Android 测试,该测试会展开叠加层,检查行是否出现,清除行,并验证行是否重置。

kotlin 复制代码
@RunWith(AndroidJUnit4::class)
class MainScreenRecompositionOverlayTest {

    @get:Rule
    val composeRule = createAndroidComposeRule<ComponentActivity>()

    @Test
    fun overlay_expandShowsCounters_and_clearResetsRows() {
        assumeTrue(BuildConfig.DEBUG)

        composeRule.setContent {
            MainScreen(viewModel = MainViewModel())
        }

        composeRule.onNodeWithText("Recompose show").performClick()
        composeRule.onNodeWithText("Clear").performClick()

        composeRule.onAllNodesWithText("TopBar:", substring = true).assertCountEquals(0)
        composeRule.onAllNodesWithText("ConnectivitySection:", substring = true).assertCountEquals(0)
    }
}

这部分内容需要仔细阐述。重构检测本身并不能完全解释启动速度的提升。它并非基线性能提升 20% 的原因,也不是后来启动速度绝对提升的主要原因。

它确实让用户界面更加清晰明了。它让我能够更快地判断状态切片和用户界面更新在最重要的屏幕上是否运行正常。这种可见性至关重要,即使最终的收益是信心而非基准测试数据。

第二阶段并非"更多基准性能分析工作"

如果图表只显示数字而不说明具体变化,这部分内容很容易被误解。

"第二阶段"的改进并非来自第二轮性能分析优化,而是来自一次清理工作,减少了启动时不必要的工作和不稳定性。严格模式有助于将这些工作呈现出来,但真正的收益来自于后续的代码和产品变更。

最重要的改进包括:

  • 移除由接收器驱动的 UI 重启路径,该路径会导致生命周期不稳定

  • 将显示设置和提供程序读取操作从关键 UI 路径中移除

  • 将阻塞事件报告进程的 I/O 操作从内联执行中移除,并为其提供备用路径

  • 阻止特权设置屏幕在交互期间频繁调用不可用的操作

这就是为什么第二阶段在两种编译模式下的性能指标都有所提升的原因。如果此次改进仅仅基于基线性能分析,我预期"速度分析"模式下的性能提升会大于"验证"模式下的性能提升。但实际上,由于底层启动路径更加健康,两种模式下的性能都得到了提升。

严格模式并未发现大量典型的启动违规,但仍然加快了启动速度

这是这项工作中最有趣的部分。我添加了仅用于调试的严格模式配置,预期会得到通常的结果:在主线程上找到明显的磁盘或网络操作,修复它们,然后就完成了启动优化。

kotlin 复制代码
internal object StrictModeInitializer {
    fun enableIfDebuggable(application: Application) {
        val isDebuggable =
            (application.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
        if (!isDebuggable) return

        StrictMode.setThreadPolicy(
            StrictMode.ThreadPolicy.Builder()
                .detectAll()
                .penaltyLog()
                .build()
        )
    }
}

但实际情况并非如此。严格模式并没有暴露出大量由应用程序引起的重大启动违规。它所做的,是将一系列相关的质量问题暴露出来:

  • 启动接收器会从后台事件重新启动 UI

  • 设置屏幕即使在没有所需权限的情况下也看起来是交互式的

  • 事件报告中阻塞进程 I/O

  • 提供程序和首选项读取操作位于关键 UI 路径上

这些问题乍一看并不像是"启动优化"。但一旦清理干净,启动速度就显著提升了。

为了隔离该阶段,我将第二阶段的代码树与一个临时副本进行了比较,该临时副本仅还原了清理阶段的文件。这样就实现了一个实用的 A/B 对比,而无需重写本地历史记录。

结果比我预期的要大:

这时,我才明白文章标题的意义所在。问题不再是"基线配置文件是否有帮助?"答案是肯定的。更有价值的问题是:究竟是什么真正改善了这款应用的启动速度?

部分答案在于基线配置文件。部分在于严格的测量规范。但还有一部分原因在于产品强化工作,这些工作消除了启动混乱,降低了主线程初始化的压力,并在应用生命周期的早期阶段停止了那些有问题的操作。

其中两项改动尤其具有代表性。

启动接收器不再强制启动 UI,现在只启动服务:

kotlin 复制代码
class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        when (intent.action) {
            Intent.ACTION_BOOT_COMPLETED,
            Intent.ACTION_LOCKED_BOOT_COMPLETED,
            Intent.ACTION_MY_PACKAGE_REPLACED -> {
                SystemEventService.start(context)
            }
        }
    }
}

显示设置状态不再在首次合成时进行完整的内联读取,现在在第一帧之后异步加载:

kotlin 复制代码
LaunchedEffect(Unit) {
    withFrameNanos { }
    displaySettings = withContext(Dispatchers.IO) { readDisplaySettingsState() }
    observeDisplaySettings = true
}

这两项更改都不算特别引人注目。它们都属于那种通常被归类为产品完整性或工程质量而非性能优化的清理工作。实际上,它们仍然改变了启动路径。

行之有效的实用工作流程

最终,我们认为可行的性能优化工作流程比完整的流程要简单得多:

  1. 测量多个启动路径。

  2. 保留基线配置文件,因为它们可以重复验证并取得成功。

  3. 在 UI 不够透明的地方添加轻量级检测。

  4. 将严格模式用作质量工具,而不仅仅是违规计数器。

  5. 在完成一些并非明显属于"性能优化"的清理工作后,再次进行基准测试。

最后一步是我最想强调的。

如果我只在基线配置更改后进行基准测试,我虽然能了解到一些正确但并不完整的信息。如果我只进行清理工作而不进行基准测试,我虽然能得到一个更漂亮的架构描述,但证据却不够充分。只有将两者结合起来,才能真正体现其价值。

结语

这个故事的简单版本是:基线配置改善了启动速度,宏基准测试证实了这一点,而重构工具则帮助优化了 Compose 的行为。这没错,但并不完整。

在这个应用中,基线配置带来的提升是最显而易见的。宏基准测试之所以重要,是因为启动过程并非只有一条路径。重构工具帮助我了解主屏幕的运行情况。但最令人惊讶的是,由严格模式 (StrictMode) 主导的清理操作,其对绝对启动速度的提升甚至超过了某些明确以性能为导向的优化工作。

这正是我会在下一个应用中沿用的部分。性能优化不仅仅在于你设定的优化目标。有时,它来自于使用合适的工具来发现那些早已存在于启动路径上的产品和生命周期问题。

这也是为什么我不再把基线配置当作一个可有可无的选项。我已经在四个应用中进行了这项工作,结果并不相同。在这个应用中,基线配置确实带来了显著的提升。而在另一个应用中,它却让启动速度变得更慢了。常见的原则并非"基准配置文件总有帮助",而是"衡量你实际拥有的应用,然后只保留那些能改进应用的功能"。

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
沐言人生3 小时前
React Native 源码分析1——HybridData 机制深度分析
android·react native
程序员陆业聪4 小时前
跨平台框架全景图:Flutter/KMP/KuiKly/RN的2026年格局
android
码云数智-园园5 小时前
Fibers(纤程)来了:打破阻塞,实现纯PHP下的异步非阻塞IO
android
shaoming37767 小时前
检查系统硬件配置是否满足PyCharm最低要求
android·spring boot·mysql
一起搞IT吧8 小时前
高通Camx功能feature分析之十五:insensor zoom介绍及实现
android·智能手机·相机
aqi009 小时前
一文读懂 HarmonyOS 6.1 带来的十大重要升级
android·华为·harmonyos·鸿蒙·harmony
秋911 小时前
MySQL 9.7.0 使用详解:新特性、实战与避坑指南
android·数据库·mysql
狼与自由11 小时前
clickhouse ReplacingMergeTree
android·clickhouse
吉吉6111 小时前
php反序列化基础知识前奏
android·php·反序列化