前言
App 启动时间是用户第一感受。打开 1 秒就见到内容 = 留下用户;打开 5 秒还在看白色启动屏 = 一秒流失。从工程角度,启动时间牵扯到进程创建、Application 初始化、Activity onCreate、首次 measure/draw/layout。本篇拆解整个启动链路,并给出"1 秒冷启动"的落地方法。
一、Android 启动的三种类型
冷启动包含进程创建,最慢;也是工程师要重点优化的。
二、冷启动链路的 7 个阶段
点击图标
│
▼
① 系统调度(< 50ms) Zygote fork 进程
│
▼
② Application 创建(50-300ms) Application.attachBaseContext
│
▼
③ Application.onCreate(最关键) 业务初始化
│
▼
④ 第一个 Activity.onCreate SplashActivity / MainActivity
│
▼
⑤ Activity.onStart / onResume 装载 ViewModel
│
▼
⑥ 首次 measure/layout/draw 渲染第一帧
│
▼
⑦ 用户可见内容
三、测量启动时间
3.1 systrace / Perfetto
# 在开发者选项启用 systrace
adb shell setprop debug.atrace.trace_flags 1
# 启动 app,30 秒后拉 trace
adb shell atrace --async_stop -o /sdcard/launch.html
adb pull /sdcard/launch.html .
看 am_proc_start、am_create_task、am_create_activity 等关键节点。
3.2 Android Vitals 启动时间
Android 9+ 在开发者选项 / 桌面 → 设置 → 开发者 → 显示 SurfaceFlinger:
adb shell am start -W com.example.app/.MainActivity
输出:
Status: ok
Activity: com.example.app/.MainActivity
ThisTime: 1923 // 从 click 到第一帧
TotalTime: 2350 // 含 Application
WaitTime: 110
3.3 自定义启动测量
class App : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
// T0: 进程开始
}
override fun onCreate() {
super.onCreate()
// T1: Application 初始化完
PerformanceTracer.mark("app_init_end")
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
if (activity is MainActivity) {
// T2: 第一帧渲染
PerformanceTracer.mark("first_frame")
}
}
})
}
}
object PerformanceTracer {
private val marks = ConcurrentHashMap<String, Long>()
fun mark(key: String) {
marks[key] = SystemClock.uptimeMillis()
log("MARK $key at ${marks[key]}ms")
}
fun duration(from: String, to: String) =
marks[to]!! - marks[from]!!
}
3.4 Baseline Profiles + Macrobenchmark 准出
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun coldStartup() {
benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
startupMode = StartupMode.COLD,
compilationMode = CompilationMode.DEFAULT,
iterations = 5,
setupBlock = { pressHome() }
) {
startActivityAndWait(Intent(Intent.ACTION_MAIN).setClassName("com.example.app", "MainActivity"))
}
}
}
生产级团队发版前必跑这个测试。
四、各阶段的优化招
4.1 Application.onCreate 中能推迟的初始化
class App : Application() {
override fun onCreate() {
super.onCreate()
// 必须同步做的:Crash 上报、Push 中必装的(如果有)
CrashReport.install(this)
// 启动器(Initializer)模式
StartupManager.register(this)
.startupAsync("Bugly") { /* ... */ }
.startupAsync("JPush") { /* ... */ }
.startupAsync("UMeng") { /* ... */ }
.startupAsync("UserCenter") { /* ... */ }
.startupOnFirstShow("Ads") { /* ... */ }
.startupOnRenderEnd("DataSync") { /* ... */ }
}
}
启动器(App Startup)框架是 Google 的官方方案,但很多公司在其之上又做了业务封装。
4.2 业务初始化的同步 vs 异步
4.3 View 扁平化
<!-- 错误:嵌套 5 层 LinearLayout -->
<LinearLayout>
<LinearLayout>
<LinearLayout>
<LinearLayout>
<LinearLayout>
<TextView/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- 正确:ConstraintLayout 一层 -->
<ConstraintLayout>
<TextView app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintLayout>
Android 渲染按 view tree 深度 O(n),层次深耗时长。
4.4 主页布局预加载
主页 drawable 可以预先 inflate:
private object MainPagePreloader {
fun preload(context: Context) {
val start = SystemClock.uptimeMillis()
LayoutInflater.from(context).inflate(R.layout.activity_main, null)
PerformanceTracer.log("mainPreload cost ${SystemClock.uptimeMillis() - start}ms")
}
}
// 在 IdleHandler 触发
Looper.myQueue().addIdleHandler {
MainPagePreloader.preload(App.context)
false
}
4.5 Splash 主题优化
<!-- res/values/themes.xml -->
<style name="Theme.Launcher" parent="Theme.Material3.DayNight">
<item name="android:windowBackground">@drawable/launch_image</item>
</style>
windowBackground 在冷启动时立即显示,避免黑屏/白屏。
注意:不能放复杂 drawable,否则 inflate 拖慢启动。
4.6 Splash Screen API
implementation("androidx.core:core-splashscreen:1.0.1")
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !isReady }
super.onCreate(savedInstanceState)
// ...
}
}
Android 12+ 推荐用 Splash Screen API 替代旧的 splash theme。
五、Baseline Profiles:启动加速的"杀手锏"
5.1 原理
Baseline Profiles 告诉 AOT 编译器预编译哪些方法,跳过 JIT 阶段。开启后,启动时间能省 30%-40%。
5.2 生成
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()
@Test
fun startupBaselineProfile() {
baselineProfileRule.collect(
packageName = "com.example.app",
stableIterations = 3,
maxStableIterations = 5,
stableIterationsTimeout = Duration.ofSeconds(30)
) {
// 让 App 走完启动路径
startActivityAndWait()
device.waitForIdle()
}
}
}
./gradlew :benchmark:pixel6Api31BenchmarkAndroidTest
# 生成 baseline-prof.txt
5.3 应用
android {
buildTypes {
release {
baselineProfile {
startup = true
}
}
}
}
每发版前重新生成一次,让基线贴合最新代码。
六、启动期间的资源分配
启动前 100ms:CPU 密集(Flash 解析)
启动 100-500ms:ClassLoader、加载资源
启动 500-1500ms:业务初始化
启动 1500ms+:Activity 渲染
定向加速:
七、多进程与启动时间
如果你用了 :push 独立进程:
android {
defaultConfig {
// 多进程 push
manifestPlaceholders["PUSH_PROCESS"] = ":push"
}
}
主进程冷启完成后,push 进程还未必要加载——直接用独立进程初始化,主进程不阻塞。
八、启动监控与告警
线上监控启动时间的曲线:
object StartupTracker {
fun track(timeMs: Long, version: String, model: String) {
val bucket = StartupBucket.get(timeMs) // bucket = < 1s, < 2s, >= 2s
FirebasePerformance.getInstance().newTrace("startup_$version")
.putMetric("duration_ms", timeMs)
.start().stop()
// 上报到自建 APM
}
}
监控:
启动时间 P50 / P95
按机型、版本、渠道分桶
灰度期间对照对照组
九、常见反模式(踩过的坑)
9.1 子线程阻塞主线程
// 错误:在 attachBaseContext 里读 SharedPreferences
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
val token = base!!.getSharedPreferences("sp", MODE_PRIVATE).getString("token", "")
// 这种 sync 读取可能在冷启动时阻塞
}
// 正确:使用 Glide 风格的 defer
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
preloadQueue.postDelayed({
sp.getString("token", "")
}, 100)
}
9.2 Application 里做磁盘 IO
// 错误
override fun onCreate() {
super.onCreate()
db.copyDatabaseFromAssets() // 100ms
loadAllConfigFromLocal() // 50ms
}
// 正确:分阶段 / 后台线程
9.3 过多的 ContentProvider
每个 ContentProvider 都是 onCreate 一次,几十个 ContentProvider 在启动期并发执行会被放大。Manifest 中合并或减少动态声明。
十、启动优化效果对比
小结
启动优化不是一招制胜,而是把"启动期内能做的所有事"重新组织一次:从进程创建时机、初始化顺序、视图渲染、Baseline 编译,每一段都有改进空间。1.5 秒冷启动 是商业 App 的当前标准。
下一篇进入卡顿监控专题——Choreographer 与 FPS 监测,从掉帧到线上监控的完整链路。
参考阅读
Android Vitals:App Startup Time
Baseline Profile 官方文档
Macrobenchmark 库使用指南