## 2. 启动耗时的根本原因:类加载、对象创建、依赖图初始化序列

应用冷启动是一个“从零到首帧”的密集任务链。工具类、业务模块、依赖注入框架背后的开销并非均匀分布,而是集中在三个连环阶段:类加载(Class Loading)、对象创建(Object Creation)以及依赖图初始化序列(Dependency Graph Initialization Sequence)。下图展示了从 Application.onCreate() 到首帧 Activity 注入完成的典型耗时分布。

启动耗时分解图 1. Application.onCreate() 2. 类加载器加载 .class 3. 构造器/反射创建对象 4. 字段赋值 / Context 注入 5. Hilt 依赖图构建 6. @Singleton 实例化 首帧 Activity 注入完成 → 开始渲染 🔵 类加载阶段 ClassLoader 查找、校验、准备 并执行静态初始化块。 🔴 对象创建阶段 构造器、反射、JNI 分配内存, 递归注入父类依赖。 🟠 依赖图初始化 Hilt 遍历所有 module 和 binding, 按依赖顺序创建单例对象。

三个阶段串行叠加,任意一个环节出现大量“冷启动首次加载”都会显著延长 displayTime。下面逐一拆解背后的机制与影响。


2.1 类加载(Class Loading)

Java/Kotlin 类的首次引用会触发 ClassLoader.loadClass()。加载过程包括:

  • 查找字节码:从 APK 的 DEX 文件中读取 .class 元数据。
  • 验证与准备:检查字节码合法性,为静态字段分配默认值。
  • 解析:将符号引用转为直接引用(符号解析可延迟,但启动时由框架触发)。
  • 初始化:执行 <clinit> 静态初始化器,包括 static 字段和 companion object 代码。

Hilt 编译时生成的组件类(如 AppComponent 实现类)通常在首次注入时才会加载。但如果入口处使用了大量依赖类,则会引发级联的类加载操作。通过 ADB 可观察实际加载量:

adb shell am start -W -P /data/local/tmp/perfetto.pb -S com.example.app/.MainActivity

利用 Android Studio Profiler 的 Class Loading 跟踪,可以定位哪些类占用了过多时间。典型瓶颈:第三方 SDK 的 Application.onCreate() 中静态初始化了重量级类。

优化提示:在冷启动路径上,优先使用 @LazyClass 或 Provider<T> 代理,避免直接引用具体实现类。Hilt 的 @Lazy 或 EntryPoints 可以有效延迟类加载到业务使用时刻。

2.2 对象创建(Object Creation)

对象创建开销包括:内存分配、构造器执行、依赖字段递归初始化。Hilt 默认通过 @Inject constructor 生成工厂类,但工厂实例化发生在首次构建依赖图时。以下是两种模式的对比:

模式 创建时机 典型耗时 对启动影响
手动 new 业务使用时 0.1–1 ms / 对象 低(分散)
Hilt 直接注入 依赖图构建时 0.2–5 ms / 对象 高(集中)
Hilt + @Lazy 首次调用 .get() 时 0.2–5 ms / 对象 低(分散)

构造器内若包含 I/O、线程锁、反射 Class.forName() 等操作,会显著放大单次创建开销。例如:

@Singleton
class HeavyAnalytics @Inject constructor() {
    init {
        // 此处做了文件读取 + 网络注册 —— 启动时缓慢!
        val config = File("/sdcard/analytics_config.json").readText()
        RetrofitClient.register(...)
    }
}
⚠ 注意:在 @Singleton 或 @ActivityScoped 的构造器中执行阻塞操作,会直接拖慢整个依赖图构建。务必遵循“构造器只做轻量赋值,复杂初始化通过 @PostConstruct 或懒加载委托”原则。

2.3 依赖图初始化序列(Dependency Graph Initialization Sequence)

Hilt 在首次调用 EntryPoints.get() 或 @HiltAndroidApp 组件时,会按以下顺序构建依赖图:

  1. 注册所有模块:扫描 @InstallIn 注解的模块,建立绑定映射。
  2. 计算依赖顺序:通过拓扑排序确定对象创建顺序。
  3. 实例化作用域绑定:所有 @Singleton、@ActivityScoped 绑定立即创建。
  4. 调用 @Provides 方法:执行带参数的提供函数,深度构造依赖链条。

该序列是导致冷启动增量的核心:一个 @Singleton 的 HttpClient 可能依赖三个拦截器,每个拦截器又依赖配置对象,形成一条长链。为解决此问题,Hilt 提供了 懒加载绑定、组件间延迟注入 等机制。下节将深入这些实现。

效果对比:通过 Perfetto 火焰图可清晰看到依赖图构建占用的 CPU 时间。开启懒加载后,该区域会大幅缩小或消失。