4. Application.onCreate 耗时定位与裁剪

Application.onCreate 耗时定位与裁剪 学习卡片

为什么 Application.onCreate 是启动耗时的“重灾区”

冷启动时,进程从 Zygote fork 出来后,系统会依次创建 Application 对象并回调 onCreate(),然后才创建首个 Activity。因此 Application.onCreate() 中任何同步耗时都会直接叠加到用户可见的首帧之前。常见的高耗时来源包括:

  • 第三方 SDK 初始化(统计、推送、崩溃上报、地图、支付等)
  • 本地数据库/SharedPreferences 的首次读写
  • 全局配置解析、字体加载、图片预解码
  • 反射扫描、注解处理、类加载

定位思路是:先量化每个初始化任务的耗时,再判断哪些必须同步、哪些可以延迟到首帧之后。下面这张图展示了从进程创建到首帧的完整时间轴,以及同步初始化与延迟初始化在时间轴上的位置差异。

Application.onCreate 启动时间轴与任务分布 时间 进程创建 Application onCreate() Activity onCreate() 首帧渲染 同步初始化任务(阻塞首帧) 崩溃SDK 统计SDK SharedPref 延迟初始化任务(首帧后执行) 推送SDK 地图SDK 字体预载 ⏱ 同步耗时 ≈ 320ms(示例) ✓ 首帧提前 ≈ 180ms(示例) 同步任务 延迟任务

用耗时埋点量化每个初始化任务

不要凭感觉判断“哪个慢”,要用 SystemClock.uptimeMillis() 或 System.nanoTime() 对每个初始化任务做包裹计时。下面是一个可直接复用的轻量计时工具类:

object StartupTimer {
    private val records = mutableListOf<Pair<String, Long>>()

    inline fun <T> measure(tag: String, block: () -> T): T {
        val start = SystemClock.uptimeMillis()
        val result = block()
        val cost = SystemClock.uptimeMillis() - start
        records.add(tag to cost)
        Log.d("StartupTimer", "$tag = ${cost}ms")
        return result
    }

    fun dump(): String =
        records.sortedByDescending { it.second }
            .joinToString("\n") { "${it.first}: ${it.second}ms" }
}

在 Application.onCreate() 中包裹每个初始化调用:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        StartupTimer.measure("CrashSDK") {
            CrashReporter.init(this)
        }
        StartupTimer.measure("StatsSDK") {
            StatsAgent.init(this)
        }
        StartupTimer.measure("SharedPref") {
            getSharedPreferences("config", MODE_PRIVATE)
                .getString("key", null)
        }
        StartupTimer.measure("PushSDK") {
            PushService.init(this)
        }
        StartupTimer.measure("MapSDK") {
            MapSdk.init(this)
        }

        Log.d("StartupTimer", "===== 启动耗时汇总 =====\n${StartupTimer.dump()}")
    }
}
埋点原则:每个 measure 块只包裹一个独立初始化任务,不要嵌套或合并,否则无法区分耗时归属。建议在 Debug 包中开启,Release 包中通过编译开关关闭日志输出。

判断哪些任务可以延迟

拿到耗时数据后,按以下标准分类:

判断维度 必须同步(留在 onCreate) 可以延迟(首帧后执行)
首帧是否依赖 崩溃上报、全局配置、主题资源 推送注册、地图定位、广告预加载
用户是否立即感知 影响首屏内容展示的初始化 后台服务、非首屏功能
是否可异步 必须在主线程完成的轻量操作 可放入子线程或 IdleHandler 的任务

延迟执行的实现方式

推荐使用 Looper.getMainLooper() 配合 MessageQueue.IdleHandler,在首帧渲染完成、主线程空闲时执行延迟任务:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        // 必须同步的:崩溃上报(首帧前需要保护)
        StartupTimer.measure("CrashSDK") {
            CrashReporter.init(this)
        }

        // 延迟到首帧后执行
        Looper.getMainLooper().queue.addIdleHandler {
            StartupTimer.measure("PushSDK") {
                PushService.init(this)
            }
            StartupTimer.measure("MapSDK") {
                MapSdk.init(this)
            }
            false // 返回 false 表示执行一次后移除
        }
    }
}
注意:IdleHandler 在主线程空闲时执行,如果延迟任务本身耗时较长,仍可能造成后续帧卡顿。对于耗时超过 50ms 的任务,建议放入子线程或使用 ExecutorService 异步执行,并确保线程安全。

验证启动收益

优化前后分别用 adb shell am start -W 测量冷启动耗时,对比 TotalTime 字段:

# 优化前
adb shell am start -W -n com.example.app/.MainActivity
# TotalTime: 820ms

# 优化后(延迟 PushSDK + MapSDK)
adb shell am start -W -n com.example.app/.MainActivity
# TotalTime: 640ms

同时观察 Logcat 中 StartupTimer 的输出,确认:

  • 同步任务耗时总和是否下降
  • 延迟任务是否在首帧之后才执行(可通过 Choreographer.postFrameCallback 打点对比)
  • 首帧渲染时间是否提前
示例数据说明:以上耗时数值(320ms、180ms、820ms、640ms)均为教学示例,实际数值取决于设备性能、SDK 版本和初始化内容。请以自己项目中的实测数据为准。