源码分析:Hilt 如何生成组件代码

这一章我们直接走进 Hilt 的编译器内部。说实话,刚接触 Dagger 那会儿,我一直觉得注解处理器是黑魔法——写个 @HiltAndroidApp,项目就能自动拿到 ApplicationComponent。后来出了 Hilt,很多团队直接把它当“Dagger 简化版”用。但问题来了:它到底帮你省了什么?背后的生成代码又长什么样?

我个人习惯是先看生成的 .java 文件,再反推处理器的逻辑。别怕,Hilt 的代码生成没那么玄乎。我带你把核心流程捋一遍。

Hilt 处理器的入口

Hilt 走的还是标准的 javax.annotation.processing 那套。它的核心处理器是 HiltJavaProcessor,继承自 BasicAnnotationProcessor。你可以在项目里搜这个类——嗯,它大概 300 行,不算长。

小技巧: 编译时加 -Aprocessor 参数,或者在 build 输出里搜 generated/source/kapt(KSP 的话路径不同),你能直接看到 Hilt 生成了什么。

处理器的核心职责就两个:

  • 收集入口点 —— 扫描所有被 @HiltAndroidApp、@AndroidEntryPoint、@HiltViewModel 标记的类。
  • 生成组件代码 —— 为每个入口点创建对应的 Hilt 组件和成员注入器。

注意,它并不会像 Dagger 那样生成一个全局的 DaggerAppComponent。Hilt 生成的组件很多,但每个都很轻量。

源码追踪:从注解到 .java 文件

我们用 @HiltAndroidApp 举个例子。你写:

@HiltAndroidApp
public class MyApp extends Application {}

Hilt 会生成一个 Hilt_MyApp 类,继承你的 MyApp。它里面长这样(简化版):

public class Hilt_MyApp extends MyApp {
  @Override
  public void onCreate() {
    super.onCreate();
    // 内部调用了 Hilt 的 ApplicationComponent 初始化
    ApplicationComponentHolder.init(this);
  }
}

等等,这里有个坑:实际你用的 Application 对象是 Hilt_MyApp,而不是你自己写的类。这就是为什么 AndroidManifest 里必须把 name 写成 .Hilt_MyApp 或 ${applicationClass}_Hilt。我之前有个项目忘了改 manifest,结果跑起来一直空指针,检查了半天才发现是 Hilt 组件没初始化。

核心数据结构:ComponentDescriptor

Hilt 内部用 ComponentDescriptor 来描述每个组件。它包含:

  • 组件名称(如 ApplicationComponent)
  • 对应的 Dagger 组件接口(如 SingletonComponent)
  • 入口点列表
  • 子组件拓扑关系

所有的 ComponentDescriptor 在编译阶段会被构建成一个 组件树。我画了张图,你可以对照着看:

Hilt 组件树与代码生成过程 @HiltAndroidApp ApplicationComponent 子组件: ActivityComponent FragmentComponent ViewComponent ↓ 处理器生成 Hilt_MyActivity.java Hilt_MyFragment.java 每个入口点生成一个 Hilt_ 前缀的宿主类,内部管理组件生命周期 树形结构:根节点 → 组件 → 子组件 → 生成代码 每个子组件都包含 @AndroidEntryPoint 注解类的注入代码

@AndroidEntryPoint 的生成逻辑

我们用 Fragment 来演示——实际上 Activity、Service、BroadcastReceiver 的逻辑类似。你写:

@AndroidEntryPoint
public class MyFragment extends Fragment { ... }

Hilt 做了三件事:

  1. 生成 Hilt_MyFragment —— 继承你的 Fragment 类,重写关键生命周期方法。
  2. 创建 ComponentBuilder —— 内部持有一个 FragmentComponentBuilder,用来组装依赖。
  3. 注入入口点 —— 在 onAttach() 中调用 FragmentComponent.inject(this)。

生成的代码大概长这样(我摘了关键部分):

public class Hilt_MyFragment extends MyFragment {
  private volatile FragmentComponentManager componentManager;

  @Override
  public void onAttach(Context context) {
    super.onAttach(context);
    // 初始化 FragmentComponent
    componentManager = new FragmentComponentManager(this);
    componentManager.inject();
  }
}

这里藏的 FragmentComponentManager 是 Hilt 的内部工具类。它负责:

  • 从父 Activity 获取 ApplicationComponent
  • 创建 FragmentComponent 实例
  • 调用生成的 Dagger 组件执行注入
注意: Hilt 生成代码的注入时机是在 onAttach() 里。如果你在 Fragment 构造函数里访问注入的字段,一定会是 null。我踩过这个坑,后来在 onCreate() 里才敢用。

组件管理的隐藏开销

可能有人觉得,Hilt 既然自动生成代码,那肯定比手写 Dagger 慢。其实不然。它的开销主要体现在两处:

阶段 真实开销 我的建议
编译期 生成代码量与 @AndroidEntryPoint 个数线性相关。每个入口点多 2~3 个 .java 文件 合理拆分模块,不要在一个 module 里堆上百个 entry point
运行时 每个 Fragment/Activity 首次创建时,会执行 Dagger 的注入流程。缓存做好后非常快 如果出现启动闪白,优先检查 Application 的初始化,而不是怀疑注入太慢

一段关键的生成代码解读

我们打开 Hilt_MyFragment 生成文件,你会发现一个 FragmentComponent 的创建方式:

private FragmentComponent createComponent() {
  return DaggerMyFragment_HiltComponents.FragmentComponent.builder()
      .applicationComponent(
          (ApplicationComponent) componentManager.getApplicationComponent())
      .fragmentContext(getContext())
      .build();
}

看到没?它实际上委托给了 Dagger 生成的 DaggerMyFragment_HiltComponents。这玩意才是真正的 Dagger 组件。Hilt 只是帮你把这些零散的部分用标准模板串起来。

我曾经做性能分析时,在 createComponent() 前后加过 trace,发现从 ApplicationComponent 获取单例的耗时几乎可以忽略(微秒级)。真正的耗时是首次反射创建 Dagger 组件时,内部要注册所有工厂方法——但只要组件是单例的,后续复用就不会有这开销。

避坑指南:同一 Activity 多个 Fragment 注入

大家注意一个问题:一个 Activity 里如果有 5 个 Fragment 都用 @AndroidEntryPoint,那每个 Fragment 都会创建一个独立的 FragmentComponent。这些组件之间是隔离的,但它们共享同一个 ApplicationComponent。这设计其实很合理——避免了跨 Fragment 的依赖混乱。我之前写过一段代码,想通过 Fragment 的共享 ViewModel 传递数据,结果因为注入的 ViewModel 作用域在同一个 Activity 上,反倒绕了圈子。后来还是老老实实用 @HiltViewModel 加 SavedStateHandle 解决的。

核心记忆点: Hilt 的代码生成就是一套模板代码的自动化。它没有引入新的依赖注入哲学,只是在 Dagger 的基础上帮你省掉了重复的组件配置、Builder 编写和注入代码调用。你的项目里每多一个 @AndroidEntryPoint,编译后就会多一个 Hilt_ 前缀的类,仅此而已。