第四讲:@HiltAndroidApp 与 @AndroidEntryPoint 到底在做什么?

说实话,我刚接触 Hilt 那会儿,对这俩注解也是一知半解。心里嘀咕:不就是加个注解吗,还能搞出什么花儿来?直到有一次线上反馈说某个 Activity 启动白屏了 500ms…… 嗯,从那以后,我就再也不敢小看这两个入口了。

咱们今天就把它们拆开来看。@HiltAndroidApp 和 @AndroidEntryPoint,本质上是在告诉 Hilt:“嘿,这里需要一个依赖注入容器,并且容器要跟你的 Android 生命周期绑在一起”。但绑定方式不同,带来的 UI 渲染开销也完全不同。

1. @HiltAndroidApp:整个应用的 DI 火车头

@HiltAndroidApp 只能加在 Application 类上。它干了三件事:

  • 生成一个 Dagger 组件,叫 HiltComponents.SingletonComponent
  • 负责管理整个应用生命周期的单例对象(比如 Retrofit、数据库)
  • 为所有 @AndroidEntryPoint 的组件提供「根作用域」

注意,这个注解的初始化时机是 Application.onCreate()。如果我在 Application 的 onCreate 里做了一大堆耗时操作,那每个 Activity 启动时都会受影响——因为 Hilt 组件要在 Application 初始化完成后才能工作。

关键影响:@HiltAndroidApp 的初始化是串行的。如果你的 Application.onCreate 里调用了 Hilt.init() 或类似方法(早期版本),那启动时的 UI 渲染会被阻塞。现在官方版本已经把初始化挪到了前台,但你还是要注意 Application 本身的加载耗时。

我个人习惯在 Application 的 onCreate 里只放最轻量的代码,比如 Crash 收集、日志初始化。像 Retrofit、Room 这种重对象,全扔给 Hilt 去懒加载,别自己手动 new。

2. @AndroidEntryPoint:让每个页面“插队”进 DI 体系

这个注解加在 Activity、Fragment、Service、BroadcastReceiver 上。它的作用是生成一个子组件(Subcomponent),挂到对应的 Hilt 组件树上。

比如一个 Activity 加上 @AndroidEntryPoint,Hilt 就会自动生成一个 Hilt_MyActivity 类,并把它跟 ActivityComponent 绑定。在这个 Activity 里用 @Inject 字段,或者用 hiltViewModel() 拿 ViewModel,都直接从子组件里取。

为什么和渲染开销有关?因为子组件的创建时机是 super.onCreate() 之前。Hilt 会在 onCreate() 被触发前执行注入工作。这个注入工作如果太重——比如要创建十几个单例,或者 Constructor 注入里做了网络请求——那你的页面 onCreate 就会被卡住。

// 正确的写法:只在 UI 线程需要时才初始化
@AndroidEntryPoint
class MyActivity : AppCompatActivity() {
    @Inject lateinit var viewModelFactory: ViewModelProvider.Factory

    override fun onCreate(savedInstanceState: Bundle?) {
        // Hilt 注入发生在 super.onCreate 之前
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 这里拿到 ViewModel 已经是注入好的
        val viewModel = ViewModelProvider(this, viewModelFactory).get(MyViewModel::class.java)
    }
}

❗ 我曾经踩过的坑:在一个 Fragment 的 @Inject 字段里注入了一个 Repository,而这个 Repository 的构造函数里调用了 SharedPreferences 的 getString()——问题是 SP 初始化也要时间。结果 Fragment 切换时卡了 200ms。排查半天才发现是 SP 的同步读取阻塞了 UI 线程。所以务必确保 Constructor 注入里的代码是轻量的,或者用 @Lazy / Provider 包装。

3. 作用域不一致,渲染开销加倍

这里有个容易被忽略的点:@HiltAndroidApp 里的 SingletonComponent 和 @AndroidEntryPoint 里的 ActivityComponent 是不同的作用域。你如果在 Activity 里注入了一个 Singleton 对象,这个对象是在 Application 层创建的,但它被第一次访问的时机却在 Activity 里。如果这个 Singleton 对象本身构造很重(比如初始化 OkHttp 连接池),那第一次打开 App 的首页就可能白屏。

看个表格梳理一下两种注解的区别:

对比维度 @HiltAndroidApp @AndroidEntryPoint
作用对象 Application 类 Activity / Fragment / Service / BroadcastReceiver
生成组件 SingletonComponent ActivityComponent / FragmentComponent 等
生命周期 整个 App 进程 对应 Android 组件的生命周期
UI 渲染影响时机 App 启动时阻塞 页面第一次打开时阻塞
推荐做法 只放必要全局对象 用 @Lazy 延迟重对象注入

你想想看,如果一个 Activity 里注入了一个 @Singleton 的 Repository,而这个 Repository 的构造里又依赖了 Room 数据库——那第一次打开这个 Activity 时,Hilt 会先创建 SingletonComponent,再创建 ActivityComponent 并装入 Repository。这个 Repository 的初始化就发生在你 UI 线程的 onCreate 之前。所以,我建议你把那些「又大又慢」的单例对象,要么用 @Lazy 包一层,要么改用 AssistedInject 延迟构造。

4. 结合实战:如何用这两个注解排查 UI 渲染开销?

我不会只在代码里加注解就完事。我会在 Application 的 onCreate 和每个 Activity 的 onCreate 入口处打上 Trace 记录,配合 Systrace 看 Hilt 的组件创建时间。具体步骤:

  1. 在 Application.onCreate 前后打印日志,看 @HiltAndroidApp 初始化花了多久。
  2. 在 Activity 的 super.onCreate() 前后打印日志,看注入过程是否卡顿。
  3. 如果某页面卡,就检查它所有的 @Inject 字段,用 Profiler 看哪个构造函数的 onMeasure 之前执行了。

我个人习惯在 Debug 构建里开启 Hilt 的 verbose 日志,能看到每个组件创建的顺序和时间。这比猜要靠谱得多。

小技巧:用 Hilt.traceComponent() 高阶 API(仅在 2.42+ 可用)可以自动在 Systrace 里标记组件创建事件,这样你在 Perfetto 里一眼就能看到是哪个子组件超时了。

下面这张流程图,把 @HiltAndroidApp 和 @AndroidEntryPoint 的协作关系以及可能产生 UI 渲染开销的点画清楚了。

@HiltAndroidApp 与 @AndroidEntryPoint 协作流程 应用启动 @HiltAndroidApp 初始化 创建 SingletonComponent Activity 启动 @AndroidEntryPoint 注入 创建 ActivityComponent 解析 @Inject 字段 构造依赖对象 UI 渲染可能被阻塞 (onCreate 耗时) ● @HiltAndroidApp 影响启动耗时,@AndroidEntryPoint 影响页面打开耗时。 ● 重对象若在 @Inject 字段直接声明,会在 Activity.onCreate 之前被创建,造成白屏。 ● 建议:将耗时注入对象用 @Lazy 包装,或在 ViewModel 中懒惰获取。

图中红色节点「UI 渲染可能被阻塞」就是我们要盯防的地方。原因往往是 @Inject 字段的依赖对象构造太重,或者是 @HiltAndroidApp 在 Application onCreate 里干了太多脏活。曾有一个项目,把 Glide 的磁盘缓存初始化放在 Application 里,结果每次冷启动都慢 300ms。后来我改成 @Singleton + @Lazy 就解决了。

总结一下:@HiltAndroidApp 和 @AndroidEntryPoint 是 Hilt 注入的入口,但入口的时机和成本直接决定了你的 UI 渲染能不能流畅。别光顾着加注解,要时刻问自己:这个对象真的需要在 onCreate 前就创建吗?

核心原则:所有 @Inject 字段都假设会在 UI 线程被注入。因此构造函数里不要做 I/O、不要 new 大对象、不要用同步锁。如果不得不这么做,那就用 @Lazy 或者 dagger.Lazy 推迟到真正使用的那一刻。