9、启动速度优化:Application初始化治理、ContentProvider启动链优化、启动耗时打点
启动速度,说白了就是用户的第一印象。我见过太多功能强大的App,就因为启动慢了半秒,用户直接划走。今天咱们就聊聊启动优化里最核心的三个环节:Application初始化、ContentProvider启动链,以及怎么精准打点。
9.1 Application初始化治理
Application的onCreate(),是启动流程的“咽喉要道”。你想想看,所有第三方SDK、业务模块的初始化代码,全堆在这里。我早期接手的一个项目,Application里光初始化就干了20多个任务,启动直接卡了3秒。
9.1.1 初始化任务分级
我个人习惯把初始化任务分成三个等级:
| 等级 | 说明 | 示例 |
|---|---|---|
| P0(必须同步) | 主线程必须立即执行,否则后续流程无法运行 | 崩溃收集、日志框架、路由表注册 |
| P1(可异步) | 不依赖主线程,可以放到子线程执行 | 图片缓存初始化、网络库配置 |
| P2(可懒加载) | 用户真正用到时才初始化 | 推送服务、位置服务、广告SDK |
核心原则:Application的onCreate()里,只保留P0级别的任务。P1用线程池异步,P2直接挪到业务入口。
9.1.2 异步初始化实战
我曾经踩过一个坑:异步初始化时,多个任务之间产生了隐式依赖。比如A任务需要B任务先创建某个单例,结果B还没跑完,A就崩了。后来我引入了初始化任务图:
// 定义任务节点
class InitTask(val name: String, val dependencies: List<String> = emptyList()) {
var runnable: (() -> Unit)? = null
}
// 构建任务图
val taskGraph = listOf(
InitTask("crash", dependencies = emptyList()),
InitTask("log", dependencies = listOf("crash")),
InitTask("network", dependencies = listOf("log")),
InitTask("image", dependencies = listOf("network"))
)
// 按拓扑排序执行
fun executeTasks(tasks: List<InitTask>) {
val sorted = topologicalSort(tasks)
sorted.forEach { task ->
if (task.dependencies.isEmpty()) {
// 无依赖的直接执行
task.runnable?.invoke()
} else {
// 有依赖的等待前置任务完成
thread {
task.runnable?.invoke()
}
}
}
}
小技巧:可以用CountDownLatch来控制依赖关系。每个任务执行完,countDown一次。依赖它的任务,await等待即可。
9.2 ContentProvider启动链优化
ContentProvider是Android启动流程里一个“隐形杀手”。很多人不知道,每个ContentProvider的onCreate(),都是在主线程、在Application.attachBaseContext()之后、Application.onCreate()之前执行的。你想想看,如果集成了10个SDK,每个SDK都声明一个ContentProvider,那启动链就变成了串行的10个初始化。
9.2.1 合并ContentProvider
我记得有个项目,启动时居然有7个ContentProvider。光初始化它们就花了800ms。解决方案很简单:合并成一个统一的Provider。
// 合并后的统一Provider
class UnifiedInitProvider : ContentProvider() {
override fun onCreate(): Boolean {
// 按优先级依次初始化
InitManager.initCrashReport(context)
InitManager.initLog(context)
InitManager.initNetwork(context)
// 其他SDK...
return true
}
override fun query(...) = null
override fun insert(...) = null
override fun update(...) = null
override fun delete(...) = null
override fun getType(...) = null
}
然后在AndroidManifest里,只保留这一个Provider,其他SDK的Provider全部移除。当然,前提是这些SDK支持自定义初始化入口。
注意:有些SDK强制要求使用自己的ContentProvider,比如Firebase。这种情况下,可以考虑用反射或者hook的方式延迟初始化。但说实话,我建议直接换SDK——启动速度比某个特定功能重要得多。
9.2.2 使用App Startup库
Google官方也意识到了这个问题,推出了App Startup库。它的核心思想就是:把ContentProvider的初始化,统一到一个入口,并且支持设置依赖顺序。
// 定义初始化器
class CrashInitializer : Initializer<Unit> {
override fun create(context: Context) {
CrashReport.init(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList() // 无依赖
}
}
class LogInitializer : Initializer<Unit> {
override fun create(context: Context) {
LogManager.init(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return listOf(CrashInitializer::class.java) // 依赖Crash
}
}
App Startup会自动解析依赖图,按顺序执行。而且它只会在AndroidManifest里声明一个Provider,大大缩短了启动链。
9.3 启动耗时打点
没有数据,优化就是盲人摸象。我见过太多团队,一上来就各种优化,结果优化完发现启动速度根本没变——因为瓶颈根本不在他们改的地方。
9.3.1 关键节点打点
我个人习惯在启动流程中埋这几个关键点:
- T0:Application.attachBaseContext() 开始
- T1:ContentProvider.onCreate() 开始
- T2:Application.onCreate() 开始
- T3:第一个Activity.onCreate() 开始
- T4:第一个Activity.onResume() 完成(用户可见)
- T5:首帧绘制完成(用户可交互)
object LaunchTimer {
private val points = mutableMapOf<String, Long>()
fun record(name: String) {
points[name] = System.currentTimeMillis()
}
fun dump() {
val sorted = points.entries.sortedBy { it.value }
val base = sorted.first().value
sorted.forEach { (name, time) ->
Log.d("LaunchTimer", "$name: ${time - base}ms")
}
}
}
// 使用
class MyApp : Application() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
LaunchTimer.record("T0_attach")
}
override fun onCreate() {
LaunchTimer.record("T2_appCreate")
super.onCreate()
// 初始化...
LaunchTimer.record("T3_appCreateEnd")
}
}
9.3.2 使用TraceView和Systrace
打点只能看到宏观数据,真要定位具体哪个方法慢,还得靠工具。我常用的两个:
- TraceView:可以看到每个方法的调用次数和耗时。适合分析CPU密集型的初始化任务。
- Systrace:可以看到系统级别的调度,比如主线程被什么阻塞了。适合分析IO、锁竞争等问题。
实战经验:我曾经用Systrace发现,启动时有个ContentProvider在读取一个100KB的配置文件,每次读都要花200ms。后来改成启动时只读必要字段,其他字段懒加载,直接省了150ms。
9.3.3 线上打点方案
线下优化完了,线上还得监控。我推荐用自定义的启动时长上报:
// 上报数据结构
data class LaunchReport(
val totalTime: Long, // 总启动时长
val providerTime: Long, // ContentProvider耗时
val appCreateTime: Long, // Application.onCreate耗时
val firstFrameTime: Long, // 首帧耗时
val deviceLevel: Int, // 设备等级(低端/中端/高端)
val version: String // App版本
)
上报后,按设备等级和版本聚合,就能看到每次发版后启动速度的变化趋势。如果某个版本启动变慢了,立刻就能定位到是哪个模块出了问题。
总结一下:启动优化不是一锤子买卖。先把Application里的P0任务梳理清楚,把ContentProvider合并或替换成App Startup,然后埋好打点,用工具定位瓶颈。这三步走完,启动速度至少能提升30%。