Android 冷启动优化实战:从 3 秒到 800毫秒 的排查路径

42 阅读3分钟

Android 冷启动优化实战:从 3 秒到 800ms 的排查路径

线上反馈"App 打开慢"是最常见也最难量化的性能问题。这篇复盘一次真实优化过程:把冷启动从 3.1 秒压到 800ms 左右,方法比结果更值得抄。

一、先把"冷启动"讲清楚

冷启动 = 进程不存在 → 启动进程 → 加载 Application → 创建首个 Activity → 首帧渲染完成。

三个关键节点:

  • T0am_proc_start 系统开始拉进程
  • T1Application.onCreate 结束
  • T2:首个 Activity onWindowFocusChanged(true) 触发

我们要压的是 T2 - T0。中间的每一步都可能是黑锅。

二、测量:不测量的优化都是玄学

只信一种数据:adb shell am start -W

adb shell am start -W -n com.demo.app/.MainActivity

输出里关注:

  • ThisTime:Activity 启动耗时
  • TotalTime:包含 Application 的总耗时
  • WaitTime:AMS 层的等待时间

再叠加代码内打点:

class App : Application() {
    override fun onCreate() {
        val t0 = SystemClock.elapsedRealtime()
        super.onCreate()
        // ... 初始化
        val cost = SystemClock.elapsedRealtime() - t0
        Log.i("Startup", "App.onCreate cost=${cost}ms")
    }
}

Activity 侧用 Choreographer.getInstance().postFrameCallback 或者 reportFullyDrawn() 抓首帧。

三、Application 层:三板斧

这轮优化里 Application 从 1.4s 降到 380ms,靠的是三件事。

1. 分级初始化

把 SDK 按"必须同步"/"可延后"/"可后台"三类拆开:

override fun onCreate() {
    super.onCreate()
    // 必须同步
    initCrashReport()
    initLogger()

    // 主线程 idle 后跑
    Looper.myQueue().addIdleHandler {
        initAnalytics()
        initImageLoader()
        false
    }

    // 后台线程
    Thread {
        initPushSDK()
        initABTest()
    }.start()
}

关键点:IdleHandler 只跑主线程能容忍的活,别把耗时 SDK 塞进去,否则只是把卡顿从启动挪到首屏交互。

2. 延迟到首屏之后

App Startup 库或者手写 ContentProvider 触发器都行,核心是把非首屏依赖的初始化推到 Activity onResume 后:

class MainActivity : AppCompatActivity() {
    override fun onResume() {
        super.onResume()
        if (!inited) {
            window.decorView.post {
                initSecondaryModules()
                inited = true
            }
        }
    }
}

3. 干掉隐式 ContentProvider 初始化

第三方 SDK 常用 <provider> 悄悄在启动阶段跑代码。检查方式:

adb shell dumpsys package com.demo.app | grep -A2 "Provider"

看到不必要的 Provider,用 tools:node="remove" 关掉,然后手动懒加载:

<provider
    android:name="com.some.sdk.InitProvider"
    android:authorities="${applicationId}.some.init"
    tools:node="remove" />

这一步单独省了 220ms,很多 SDK 的 Provider 其实完全不需要跟启动绑定。

四、首屏 Activity:布局和数据都要瘦

Application 优化到位后,Activity 那 900ms 就成了瓶颈。

1. 布局层级和 measure/layout

用 Layout Inspector 或 hierarchyviewer 看首屏层级。这次踩到两个坑:

  • 一个 ConstraintLayout 里嵌了 5 层 LinearLayout,重构成扁平 Constraint 后省 90ms
  • include 了一个巨大的通用头部布局,里面 90% 视图 visibility=gone,改成 ViewStub 懒加载省 60ms

2. 主线程数据源要异步

首屏依赖的接口不要在 onCreate 里同步等。用 Flow + Loading 骨架屏:

class HomeViewModel : ViewModel() {
    val state = MutableStateFlow<UiState>(UiState.Loading)

    init {
        viewModelScope.launch {
            state.value = try {
                UiState.Success(repository.load())
            } catch (e: Exception) {
                UiState.Error(e)
            }
        }
    }
}

Activity 侧先渲染骨架,数据回来再刷。用户感知的"启动完成"不是数据完成,是首帧可见

3. 主题闪屏别用 Activity

老代码里用 SplashActivityMainActivity 跳转,光 Activity 创建就多花 150ms。改成 Theme 层的 windowBackground

<style name="AppTheme.Launcher" parent="AppTheme">
    <item name="android:windowBackground">@drawable/launch_bg</item>
</style>

MainActivity 用 Launcher 主题,super.onCreate 后立刻切回 AppTheme。省一个 Activity,稳赚。

五、验证和防回归

优化完不做防回归,三个月后必然被打回原形。加两道闸:

1. CI 里加启动耗时基线

am start -W 跑 20 次,取 P90,超过基线 15% 直接失败。这套脚本可以放到 GitHub Actions 或 Jenkins。

2. 线上监控

上报 Application.attachBaseContextonWindowFocusChanged 的耗时,按机型分位统计。低端机 P90 才是真实体验,别只看平均值。

object StartupTracker {
    private var t0 = 0L
    fun mark() { t0 = SystemClock.elapsedRealtime() }
    fun report(tag: String) {
        val cost = SystemClock.elapsedRealtime() - t0
        Analytics.log("startup", mapOf("tag" to tag, "cost" to cost))
    }
}

六、复盘:哪些是"看起来很美"的优化

有几种做法要谨慎:

  • 多 dex 优化 / MultiDex.install 提前:现在 minSdk ≥ 21 基本不用管
  • StrictMode 关掉:只是把问题藏起来,别关,改成 release 关 debug 开
  • 反射调用系统 API 跳过初始化:兼容性成本远大于收益
  • 提前预加载类:意义不大,dex2oat 之后差异不明显

真正立竿见影的还是那三件:减少 Application 里的同步工作首屏布局扁平化用主题闪屏代替 SplashActivity

小结

冷启动优化没什么魔法:测量 → 拆解 → 分级 → 防回归。别一上来就找玄学方案,先把 am start -W 跑起来,把 Application 里的初始化列个表,砍掉一半你就赢了一半。