Android 性能优化:02.启动时长优化

一、启动时长的重要性

启动是用户与 App 的第一次交互,也是流失率最高的路径之一。业界的共识数据:

  • 冷启动每多 1 秒,用户的等待焦虑呈非线性上升;Google 官方建议冷启动首帧(TTID)控制在 500ms 以内为优秀,2s 以上即为慢启动
  • Google Play Vitals 对启动有明确的"坏行为"阈值,会影响应用在商店的推荐权重:
    • 慢冷启动:≥ 5s
    • 慢温启动:≥ 2s
    • 慢热启动:≥ 1s
  • 启动过程集中了 App 架构中最多的"历史债务":SDK 初始化堆叠、主线程 IO、布局臃肿,是投入产出比最高的优化场景。

优化方法:先测量、再优化、后监控,不要在没数据的情况下动手。


二、核心概念

2.1 三种启动类型

这是面试和优化工作的基础概念,三者耗时差异巨大,测量时必须区分:

类型前提条件系统行为典型耗时
冷启动进程不存在(开机后首次、被系统回收、被用户杀掉)创建进程 → 创建 Application → 创建并绘制首个 Activity最慢(优化主战场)
温启动进程存活,但 Activity 已被销毁(如按返回键退出后再次进入)复用进程,重新创建 Activity中等
热启动进程与 Activity 都存活(按 Home 键后切回)仅将 Activity 带到前台(onRestart→onStart→onResume)最快

常见误区:很多"优化成果"其实是拿热启动数据冒充冷启动数据。规范的冷启动测量姿势是先 adb shell am force-stop <pkg>,或静置等待系统回收。

2.2 两个关键指标:TTID 与 TTFD

  • TTID(Time To Initial Display,初始显示时间):从启动到第一帧绘制完成的时间。由系统自动统计,logcat 中的 Displayed 日志和 am start -W 的 TotalTime 都是它。它只代表"屏幕上出现了东西",不代表页面可用。
  • TTFD(Time To Full Display,完全显示时间):从启动到首屏内容完整加载渲染(如列表数据填充、首图加载完)的时间。系统不知道你的业务何时算"完成",需要开发者手动调用 Activity.reportFullyDrawn() 上报。

优化实践中:用 TTID 做系统侧基准,用 TTFD 做用户体验基准,两者缺一不可。只优化 TTID 很容易出现"白屏快、内容慢"的假优化。

2.3 冷启动全链路(源码视角)

点击桌面图标到首帧上屏,完整链路如下:

用户点击图标
  └─ Launcher 进程 → startActivity
       └─ AMS(ATMS) 校验、创建 ActivityRecord、任务栈管理
            └─ 进程不存在 → Zygote.fork() 孵化新进程
                 └─ 新进程入口:ActivityThread.main()
                      ├─ 初始化主线程 Looper
                      └─ attach() → AMS.attachApplication()
                           └─ bindApplication(绑定进程)
                                ├─ LoadedApk.makeApplication() 创建 Application
                                ├─ Application.attachBaseContext()   ← 应用代码最早执行点
                                ├─ installContentProviders()          ← 所有 ContentProvider 在此创建
                                └─ Application.onCreate()             ← SDK 初始化重灾区
                                └─(若有后台进程组件则再次循环以上流程 → 多进程初始化问题)
            └─ launchActivity
                 └─ Activity 生命周期:onCreate → onStart → onResume
                      ├─ setContentView:解析 XML、反射/LayoutInflater 实例化 View 树
                      ├─ Window 创建、DecorView 挂载
                      └─ measure / layout / draw → Vsync → 首帧上屏(TTID)

上图链路的各步骤都是一个可测量、可优化的耗时点,后文的所有手段都是在这条链路的某一段上做文章。

2.4 冷启动耗时的四大构成

  1. 进程创建与类加载:fork、类校验、dex 解释执行/JIT。未做编译优化的应用,首次启动大量代码走解释执行,这是"安装后第一次启动特别慢"的根因。
  2. Application 阶段:ContentProvider 创建(注意:每个 Provider 都是串行初始化,且三方 SDK 常偷偷塞 Provider)、Application.onCreate 里的 SDK 同步初始化。
  3. Activity 与布局阶段:XML 解析、View 树构建、主题与资源加载、首帧绘制。
  4. 数据等待阶段:首屏接口、本地缓存读取、图片解码,影响的是 TTFD。

2.5 必须掌握的技术名词

  • Zygote:所有应用进程的"母体",通过 fork 复用预加载的框架类与资源,降低进程创建成本。
  • ART 编译策略(JIT/AOT):Android 7.0+ 采用混合编译。安装时只编译热点代码,首次启动大量走解释执行 + JIT,热点积累后再 AOT。这正是 Baseline Profile 要解决的问题
  • Baseline Profile(基准配置文件):把"启动和关键路径会用到哪些类和方法"提前告诉系统,安装/闲时即完成 AOT 预编译,官方数据可带来约 30% 的启动速度提升,是目前性价比最高的一招,后面单独展开。
  • Cloud Profile:Google Play 聚合大量真实设备生成的配置文件,随安装分发,与本地 Baseline Profile 叠加生效。
  • ContentProvider 自动初始化:任何在 manifest 注册 Provider 的 SDK(WorkManager、Lifecycle、不少三方 SDK)都会在 Application.onCreate 之前被串行创建,是不可见的耗时大户。
  • 多进程初始化:App 的每个进程启动都会执行一遍 Application 生命周期,只按主进程设计初始化逻辑会造成浪费甚至 crash。

三、测量:先把耗时变成数字

3.1 adb 命令(最快的线下粗测)

# 先确保冷启动环境
adb shell am force-stop com.example.app
# 启动并输出耗时
adb shell am start -W -n com.example.app/.MainActivity

输出解读:

ThisTime: 812    # 最后一个 Activity 启动到首帧的耗时
TotalTime: 812   # 新应用启动总耗时(含进程创建),冷启动看这个
WaitTime: 855    # 系统返回总耗时,含前一个 Activity 的 pause,一般 ≥ TotalTime

注意:单次测量波动大,至少测 10 次取中位数,且要固定机型、清后台、关闭网络代理等变量。

3.2 logcat 系统日志

adb logcat | grep -E "Displayed|Fully drawn"
  • Displayed com.example.app/.MainActivity: +812ms → TTID
  • Fully drawn ...(需在代码中调用 reportFullyDrawn())→ TTFD
// 在首屏数据渲染完成的时机上报(如列表 Adapter 数据填充后、首帧回调中)
lifecycleScope.launch {
    val data = repository.loadHomeData()
    adapter.submitList(data)
    recyclerView.doOnPreDraw { reportFullyDrawn() }
}

3.3 代码内打点(自定义指标)

class MyApp : Application() {
    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)
        // API 24+ 可直接拿到真实的进程启动时刻,比自己打点更准
        LaunchTimer.processStartMs = Process.getStartElapsedRealtime()
        LaunchTimer.mark("attachBaseContext")
    }

    override fun onCreate() {
        super.onCreate()
        LaunchTimer.mark("app_onCreate_end")
    }
}

// Activity 首帧
window.decorView.doOnPreDraw {
    LaunchTimer.mark("first_frame")
    LaunchTimer.dump()  // 输出各阶段耗时
}

这套打点稍作封装(结合 BuildConfig 开关 + 上报通道)就是线上监控的雏形。

3.4 Perfetto / Systrace(定位耗时细节的核心武器)

Android 10+ 推荐 Perfetto。录制启动 trace:

# 方式一:命令行(推荐用 perfetto.dev 网页的 Record 功能生成配置)
adb shell perfetto -o /data/misc/perfetto-traces/startup.pftrace -t 20s \
  am wm ss view res dalvik sched freq idle binder_driver

adb pull /data/misc/perfetto-traces/startup.pftrace
# 拖到 https://ui.perfetto.dev 分析

在代码中打自定义 trace 点,让业务逻辑出现在 trace 里:

import androidx.tracing.Trace

Trace.beginSection("initAdSdk")
AdSdk.init(this)
Trace.endSection()

分析时重点看:bindApplicationactivityStartChoreographer#doFrame、主线程的 Runnable/IO 段、锁等待(monitor contention)。一条原则:主线程在启动链路上出现的每一毫秒都要问一句"它必须现在做吗"。

3.5 Macrobenchmark(可复现、可进 CI 的基准测试)

Jetpack Macrobenchmark 是目前最规范的启动测量方案,可自动清编译状态、循环测量、输出统计分布,并能在 CI 里做回归:

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

    @Test
    fun coldStartup() = benchmarkRule.measureRepeated(
        packageName = "com.example.app",
        metrics = listOf(StartupTimingMetric()),
        compilationMode = CompilationMode.Partial(), // 贴近真实用户:带 Baseline Profile 的编译状态
        startupMode = StartupMode.COLD,
        iterations = 10
    ) {
        pressHome()
        startActivityAndWait()
    }
}

CompilationMode 的选择很有讲究:None() 模拟无编译优化(最差情况)、Partial() 模拟真实用户(Baseline Profile 生效)、Full() 全量 AOT(最好情况)。对比 None 和 Partial 的结果,就能量化 Baseline Profile 的收益。

3.6 线上监控

  • Android Vitals(零成本):Play Console 直接查看冷/温/热启动的分布与慢启动会话占比,按机型、系统版本拆分。
  • 自研/三方 APM:基于 3.3 的打点上报 TTID/TTFD,核心看 P50 / P90 分位值而非平均值(平均值会被少量极端样本带偏);高端机优化好了不代表低端机没问题,分机型看。
  • 开源方案参考:Matrix(微信)、Booster(滴滴,含字节码插桩能力)。

四、优化步骤总览(方法论)

Step 1 建指标:定义 TTID / TTFD,选定基准机型(主力机型 + 一款低端机)
Step 2 测基线:Macrobenchmark + Perfetto 记录优化前数据,输出 trace 火焰图
Step 3 拆链路:把启动耗时按「进程创建 / Application / Activity / 数据」四段拆解归因
Step 4 逐段优化:先收益大的(通常 Application 初始化 + Baseline Profile)
Step 5 对比验证:同条件复测,确认 P50/P90 下降且无功能回归
Step 6 防劣化:benchmark 进 CI + 线上分位值告警

优化次序经验法则:测量工具链搭建 → Baseline Profile(改动小收益大)→ Application 初始化治理 → 首屏布局与数据 → 进阶新技术。


五、分阶段优化手段(实战清单)

5.1 编译与代码层面:优先 Baseline Profile

这是当前官方最推荐、投入产出比最高的手段。原理:把启动路径的热点类和方法打进配置文件,系统在安装后/空闲时直接 AOT 编译,绕开解释执行和 JIT 预热期。

接入方式(AGP 8.x / Baseline Profile Gradle 插件):

// app/build.gradle.kts
baselineProfile {
    // 自动生成基准配置文件的开关
    automaticGenerationDuringBuild = true
}

新建 baselineprofile 模块(Android Studio 有现成模板:New Module → Baseline Profile Generator),编写生成用例:

@RunWith(AndroidJUnit4::class)
class StartupBaselineGenerator {
    @get:Rule
    val baselineProfileRule = BaselineProfileRule()

    @Test
    fun generate() = baselineProfileRule.collect(
        packageName = "com.example.app"
    ) {
        pressHome()
        startActivityAndWait()
        // 可以顺便把登录后首页、核心二级页也走一遍,收益更大
    }
}

执行 ./gradlew :app:generateReleaseBaselineProfile,生成的 baseline-prof.txt 会打进 APK/AAB。随后用 Macrobenchmark 对比 CompilationMode.None()Partial() 验证收益。

配套动作:

  • 开启 R8 全模式 + 代码/资源缩减isMinifyEnabled / isShrinkResources),减小 dex 体积就是减小类加载和校验成本。
  • 老项目若还在 Multidex,升级 minSdk 到 21+ 直接去掉 MultiDex,否则主 dex 裁剪和 dex 布局优化(dex layout)也值得做(ReDex / Booster 都有成熟方案,原理是把启动用到的类排到主 dex 前部,减少 IO 页缺失)。

5.2 Application 阶段:初始化治理(多数 App 的最大收益点)

(1) 统计并消灭 ContentProvider 自动初始化

manifest 里每个 Provider 都在 Application.onCreate 之前串行创建。先在 trace 里数一遍有哪些 Provider(installContentProviders 段),处理思路:

  • 三方 SDK 若只是为了初始化而注册 Provider,用 tools:node="remove" 移除,改为手动初始化:
<provider
    android:name="com.some.sdk.SdkInitProvider"
    android:authorities="${applicationId}.some-sdk"
    tools:node="remove" />
  • 自己模块的初始化统一收敛到 Jetpack App Startup,它把所有 initializer 合并进一个 InitializationProvider
class AdSdkInitializer : Initializer<Unit> {
    override fun create(context: Context) { AdSdk.init(context) }
    override fun dependencies(): List<Class<out Initializer<*>>> =
        listOf(ConfigInitializer::class.java)  // 声明依赖,自动拓扑排序
}

(2) 初始化任务分级治理

把 onCreate 里的所有任务按"谁在什么时候需要它"分三级:

级别定义策略典型例子
P0 必需首帧绘制前必须完成保留主线程同步执行,只留极少量崩溃监控、进程名判断、基础配置
P1 首屏后首屏可用前需要,但不挡首帧IdleHandler / 首帧后延迟队列埋点上报通道、AB 实验下发
P2 懒加载用到才需要使用时初始化(双重检查锁/单例)广告 SDK、推送、支付、WebView 池
// P1 示例:主线程空闲时才执行,不抢首帧
Looper.myQueue().addIdleHandler {
    TrackerSdk.init(context)
    false  // 返回 false 执行一次即移除
}

(3) 异步与并行

  • P0 内可并行的任务丢进启动线程池;注意线程池别贪大(启动期 CPU 本来就在抢),且子线程任务要考虑 Application 持有生命周期,避免 Activity 回调空指针。
  • 进阶方式是引入启动任务编排框架(DAG 有向无环图,参考阿里 Alpha / 自研 AnchorTask 思路):任务声明依赖关系与执行线程,框架统一调度,既解决"先后顺序靠注释维护"的混乱,也方便输出各任务耗时报表。
  • Kotlin 的 by lazy 只是"访问时初始化",如果首次访问仍在主线程关键路径上,一样是耗时。

(4) 消灭主线程 IO 与锁

  • SharedPreferences 在启动期高频读写是经典坑:单文件过大首次加载全量解析、跨进程 MODE_MULTI_PROCESS 已废弃。方案:拆分文件 + 首屏只读必需项,或迁移 MMKV / DataStore
  • 启动期禁做网络请求、数据库重操作;确有需求走异步。
  • trace 里搜 monitor contention 看主线程锁等待,常见来源是单例的双重检查锁和 OkHttp 内部锁。

(5) 多进程适配

val processName = Application.getProcessName()  // API 28+;低版本读 /proc/self/cmdline
if (processName == packageName) {
    // 只有主进程才做全量初始化
}

推送进程、WebView 进程、下载进程各自只做自己需要的初始化,能直接砍掉重复成本。

5.3 首屏 Activity 阶段

(1) 布局减载

  • 减少层级与过度嵌套,ConstraintLayout/FrameLayout 打平层级;能用 <merge> 的地方别多套一层。
  • 非首屏 View 用 ViewStub 延迟 inflate。
  • 首页布局重的场景用 AsyncLayoutInflater 异步构建 View 树(注意回调时机与主题兼容)。
  • 新项目直接 Compose,配合 Baseline Profile 的收益更明显(Compose 官方库自带 profile 规则)。

(2) 启动预览页(视觉体验优化)

秒开的"障眼法":给启动 Activity 配置带品牌图的启动主题,系统窗管在应用内容 ready 前先绘制 windowBackground:

<style name="Theme.App.Launch" parent="Theme.Material.NoActionBar">
    <item name="android:windowBackground">@drawable/launch_screen</item>
</style>
override fun onCreate(savedInstanceState: Bundle?) {
    setTheme(R.style.Theme_App)  // setContentView 之前换回真实主题
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}

(3) SplashScreen API(Android 12+ 必做适配)

Android 12 起系统强制展示开屏页,接入 androidx.core:core-splashscreen(向下兼容):

override fun onCreate(savedInstanceState: Bundle?) {
    val splash = installSplashScreen()   // 必须在 super.onCreate 之前
    super.onCreate(savedInstanceState)
    // 关键:让开屏页停留到数据就绪,避免"开屏 → 白屏 → 内容"的三段跳
    var ready = false
    splash.setKeepOnScreenCondition { !ready }
    viewModel.uiState.onEach { ready = it.loaded }.launchIn(lifecycleScope)
}

setKeepOnScreenCondition 是把 TTID 和 TTFD 平滑衔接起来的官方方案,强烈推荐。

(4) 首帧之后的事别提前

onCreate 里只放首帧必需逻辑;onWindowFocusChanged、首帧回调之后再触发二阶段任务(预加载下一级页面、初始化 WebView 池等)。

5.4 数据与网络层面(TTFD 优化)

  • 首屏数据预取 + 缓存直出:冷启动先渲染本地缓存快照,接口回来后增量刷新,TTFD 立即可用。
  • 首屏接口前置:在闪屏阶段就发出首屏请求(甚至 Application 阶段发起、Activity 消费结果),让网络和 UI 初始化并行。
  • 减少首屏串行请求链,能合并的接口合并;图片用缩略图 + 渐进式加载。
  • 广告开屏场景:广告请求与应用初始化并行,但要做好超时熔断,别让广告 SDK 把首屏拖住(超时直接进首页)。

5.5 进阶手段(头部 App 玩法,按需了解)

  • 类加载优化:基于 Baseline Profile 的 dex 布局重排(Redex Interdex / Booster dextral),把启动类聚合到主 dex 前部,减少 mmap 页缺失 IO。
  • 启动期 GC 抑制 / 线程优先级调整:短暂提升主线程优先级、延后非关键线程,收益因机而异,需 A/B 验证。
  • 资源加载优化:预加载首屏 drawable、字体懒加载(Downloadable Fonts 注意首屏阻塞)。
  • WebView 预热池:首屏含 WebView 的 App 常用,在空闲期预创建 WebView 实例(注意多进程与内存开销)。
  • 15/16KB 页大小适配:新设备内存页增大后,native 库对齐与 so 体积会影响加载耗时,打包时留意 16KB 对齐支持。
  • 保活/热启动占比提升:属于体验博弈手段,合规前提下谨慎使用,不展开。

六、持续监控与防劣化

  1. CI 卡点:Macrobenchmark 接入 CI,核心指标(TTID P50/P90)回归超过阈值(如 +10%)直接报警或阻断合入。
  2. 线上分位值告警:看 P90 而不只是均值,分机型/系统版本/版本号维度对比。
  3. 新增 SDK 评审机制:任何 SDK 接入前回答三个问题——是否在启动链路上初始化?是否注册了 ContentProvider?能否懒加载?
  4. 版本复盘:每个大版本出一页启动数据对比,固化到团队 Wiki。

七、常见误区清单

  • ❌ 用热启动/温启动数据汇报"优化成果"
  • ❌ 只测一次、只在旗舰机上测
  • ❌ 在 Debug 包上测性能(Debug 关闭了 JIT/AOT 优化和 R8,数据无意义,一律用 release 包 + 关闭 Instant Run
  • ❌ 只看 TTID 不看 TTFD,搞出"白屏秒出、内容三秒"的假快
  • ❌ 不看 trace 凭直觉优化,把时间花在收益最小的环节
  • ❌ 接了优化却不上 Baseline Profile,白白放弃 30% 的官方红利

八、总结

启动优化 = 链路(fork → Application → Activity → 首帧 → 数据就绪)× 尺子(TTID / TTFD)× 三板斧(Baseline Profile、初始化治理、首屏减载)× 一套护栏(CI benchmark + 线上分位值告警)

参考资料

  • Android 官方文档:App startup time(developer.android.com/topic/performance/vitals/launch-time)
  • Android 官方文档:Baseline Profiles 与 Macrobenchmark
  • Android Vitals:developer.android.com/topic/performance/vitals
  • Perfetto 官方文档:perfetto.dev
  • 《Android 移动性能实战》(腾讯 SNG 团队)、支付宝/手淘/抖音启动优化技术分享
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值