一、启动时长的重要性
启动是用户与 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 冷启动耗时的四大构成
- 进程创建与类加载:fork、类校验、dex 解释执行/JIT。未做编译优化的应用,首次启动大量代码走解释执行,这是"安装后第一次启动特别慢"的根因。
- Application 阶段:ContentProvider 创建(注意:每个 Provider 都是串行初始化,且三方 SDK 常偷偷塞 Provider)、Application.onCreate 里的 SDK 同步初始化。
- Activity 与布局阶段:XML 解析、View 树构建、主题与资源加载、首帧绘制。
- 数据等待阶段:首屏接口、本地缓存读取、图片解码,影响的是 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→ TTIDFully 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()
分析时重点看:bindApplication、activityStart、Choreographer#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 对齐支持。
- 保活/热启动占比提升:属于体验博弈手段,合规前提下谨慎使用,不展开。
六、持续监控与防劣化
- CI 卡点:Macrobenchmark 接入 CI,核心指标(TTID P50/P90)回归超过阈值(如 +10%)直接报警或阻断合入。
- 线上分位值告警:看 P90 而不只是均值,分机型/系统版本/版本号维度对比。
- 新增 SDK 评审机制:任何 SDK 接入前回答三个问题——是否在启动链路上初始化?是否注册了 ContentProvider?能否懒加载?
- 版本复盘:每个大版本出一页启动数据对比,固化到团队 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 团队)、支付宝/手淘/抖音启动优化技术分享

1020

被折叠的 条评论
为什么被折叠?



