12、依赖注入框架原理:Dagger/Hilt的注解处理流程、自定义简易DI框架

依赖注入(DI)这个词,刚入行那会儿我觉得挺玄乎的。说白了就是「我要用什么东西,你别自己 new,让别人给你送过来」。但真正让我理解 DI 框架价值的,是一次重构老项目——一个 Activity 里 new 了十几个对象,改一个构造参数,满屏飘红。嗯,从那以后,我再也不敢手写依赖管理了。

今天我们就来扒一扒 Dagger 和 Hilt 的注解处理流程,然后自己动手写一个迷你 DI 框架。你会发现,那些看似高深的注解处理器,其实没那么神秘。

12.1 Dagger/Hilt 的注解处理流程

Dagger 和 Hilt 都是编译期 DI 框架。它们不是在运行时用反射去注入,而是在编译期就生成好代码。这一点和 Spring 那种运行时注入完全不同。

我个人习惯把 Dagger 的处理流程分为三个阶段:

阶段 触发时机 核心工作
注解解析 javac 编译期 扫描 @Module、@Provides、@Inject、@Component 等注解
依赖图构建 注解处理器中 分析依赖关系,检测循环依赖、缺失绑定
代码生成 编译期最后阶段 生成 DaggerXxxComponent 等工厂类

你想想看,如果运行时才去检查依赖有没有配全,那用户一打开 App 就崩了,多尴尬。Dagger 把这件事提前到编译期,说白了就是「编译不过就别想跑」。

12.1.1 @Inject 和 @Module 的处理差异

这里有个容易混淆的点:@Inject@Module 虽然都是提供依赖的方式,但处理逻辑完全不同。

  • @Inject 标注构造方法:注解处理器直接读取构造参数列表,生成对应的 Factory 类。比如 @Inject constructor(val repo: Repository),会生成 UserViewModel_Factory
  • @Module 标注的 @Provides 方法:处理器会解析方法的返回类型和参数,生成对应的 Provider 类。比如 @Provides fun provideApi(): Api,会生成 AppModule_ProvideApiFactory

我在项目中遇到过一个问题:某个 @Provides 方法返回的是接口类型,但实际注入点需要的是具体实现类。Dagger 编译期直接报错,提示「类型不匹配」。当时我还觉得 Dagger 太严格,后来想想,这其实是帮我们提前发现了 bug。

12.1.2 Hilt 的封装简化

Hilt 是在 Dagger 基础上封装的。它做了两件事:

  1. 自动生成 @Component,你不用再手写 Component 接口了。
  2. 通过 @HiltAndroidApp@AndroidEntryPoint 自动管理 Android 组件的生命周期。

说白了,Hilt 就是 Dagger 的「傻瓜版」。但底层原理完全一样,都是编译期注解处理 + 代码生成。

核心结论:Dagger/Hilt 的注解处理器在编译期做了三件事——扫描注解、构建依赖图、生成工厂类。运行时只做一件事:调用生成的代码完成注入。

12.2 自定义简易 DI 框架

光说不练假把式。我们来手写一个迷你 DI 框架,名字就叫 MiniInject。它只做两件事:

  • 通过注解标记需要注入的依赖
  • 在编译期生成注入代码

先定义注解:

// 标记需要注入的字段
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class MiniInject

// 标记提供依赖的模块
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class MiniModule

// 标记提供依赖的方法
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class MiniProvides

然后写注解处理器:

@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.MiniInject", "com.example.MiniModule")
class MiniInjectProcessor : AbstractProcessor() {

    override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {
        // 1. 收集所有 @MiniModule 标注的类
        val modules = roundEnv.getElementsAnnotatedWith(MiniModule::class.java)
        
        // 2. 收集所有 @MiniInject 标注的字段
        val injectFields = roundEnv.getElementsAnnotatedWith(MiniInject::class.java)
        
        // 3. 构建依赖映射表
        val dependencyMap = mutableMapOf<String, String>()
        
        for (module in modules) {
            val moduleElement = module as TypeElement
            for (enclosed in moduleElement.enclosedElements) {
                if (enclosed.getAnnotation(MiniProvides::class.java) != null) {
                    val method = enclosed as ExecutableElement
                    val returnType = method.returnType.toString()
                    val methodName = method.simpleName.toString()
                    dependencyMap[returnType] = "$moduleElement.$methodName()"
                }
            }
        }
        
        // 4. 生成注入代码
        for (field in injectFields) {
            val fieldElement = field as VariableElement
            val fieldType = fieldElement.asType().toString()
            val provider = dependencyMap[fieldType]
            
            if (provider != null) {
                // 生成类似 target.field = provider() 的代码
                generateInjectCode(fieldElement, provider)
            } else {
                processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, 
                    "找不到 $fieldType 的提供者,请检查 @MiniModule 配置")
            }
        }
        
        return true
    }
    
    private fun generateInjectCode(field: VariableElement, provider: String) {
        val packageName = processingEnv.elementUtils.getPackageOf(field).toString()
        val className = "MiniInjector_${field.simpleName}"
        
        val code = """
            package $packageName
            
            object $className {
                fun inject(target: ${field.enclosingElement}) {
                    target.${field.simpleName} = $provider
                }
            }
        """.trimIndent()
        
        // 写入文件(省略具体 FileObject 操作)
    }
}

避坑指南:我曾经在写注解处理器时,忘记处理多轮编译(round)的情况。结果第一次编译能生成代码,第二次编译就报「重复文件」。解决方案是在 process() 方法里判断 roundEnv.processingOver(),只在最后一轮生成代码。

12.3 依赖注入的核心流程图

下面这张图展示了 MiniInject 框架从注解扫描到代码生成的完整流程:

阶段一:注解扫描 扫描 @MiniInject 扫描 @MiniModule 扫描 @MiniProvides 阶段二:依赖图构建 构建类型→提供者映射 检测缺失绑定 检测循环依赖 阶段三:代码生成 生成 Factory 类 生成 Injector 类 写入 .java 文件 编译错误:依赖缺失 编译通过 ✓ MiniInject 框架编译期处理流程 关键点:所有依赖检查都在编译期完成 运行时只做赋值操作,无反射开销 如果编译通过,运行时绝对不会出现「找不到依赖」的崩溃

12.4 运行时注入的实现

编译期生成代码后,运行时怎么用?很简单,调用生成的 Injector 类:

class MainActivity {
    @MiniInject
    lateinit var api: Api
    
    fun onCreate() {
        MiniInjector_api.inject(this)
        // 此时 api 已经被赋值了
        api.fetchData()
    }
}

你看,运行时没有任何反射调用,就是直接赋值。这也是 Dagger 性能好的原因——它把「找依赖」这件事提前到了编译期,运行时就是一行 target.field = provider()

注意:自定义 DI 框架虽然能帮你理解原理,但千万别用在生产环境。Dagger/Hilt 经过了大量优化,比如缓存单例、处理作用域、支持懒加载等。我当年自己写的 MiniInject 只支持单例,连 Activity 生命周期都没处理,上线后内存泄漏惨不忍睹。

12.5 总结

依赖注入框架的核心就两件事:

  • 编译期:用注解处理器扫描注解,构建依赖图,生成工厂类。
  • 运行时:调用生成的代码完成赋值,零反射开销。

Dagger 和 Hilt 的区别在于,Hilt 帮你省去了写 Component 的麻烦,但底层原理一模一样。你如果理解了注解处理器的 process() 方法怎么收集信息、怎么生成代码,那 Dagger 对你来说就不再是黑盒了。

我个人建议,如果你有时间,可以照着上面的思路自己写一个 MiniInject。不用太复杂,能处理单例和基本类型就行。写完之后你再去看 Dagger 的源码,会发现那些 @Component@Subcomponent 的处理逻辑,本质上和你写的 MiniInject 是一样的——只是人家考虑的场景更多、生成的代码更优化。

嗯,今天就到这里。记住一句话:依赖注入不是魔法,是编译期帮你写好了本该你手写的代码。


公众号:蓝海资料掘金营,微信deep3321