一、 背景
最近在业务开发中使用 DI,发现自己对它的理解大多还停留在“会用”的层面。为了告别盲区,于是了解学习了一些DI的作用与实现。现将这些内容整理出来,希望帮助到一些朋友。
二、 DI介绍
假如在不使用 DI 的情况下,类需要自行管理依赖的创建,例:
class UserViewModel {
// 强耦合:ViewModel 必须知道如何创建 Repository 以及它需要的所有参数
private val repository = UserRepository(LocalDatabase(), ApiService())
}
这种模式会导致以下三大痛点:
- 单元测试困境:由于依赖被硬编码在内部,无法在测试时注入 Mock 对象,导致测试难以隔离。
- 代码僵化:底层修改(如 Repository 增加一个网络拦截器参数)会引发上层所有调用方代码的连锁修改。
- 生命周期错位:在 Activity 中手动管理单例或局部实例极易引发内存泄漏或状态不一致。 依赖注入本质上是实现控制反转(IoC)的一种手段。它将“创建对象”的权力从类内部移交给了外部容器。
DI 的核心价值
- 彻底解耦:类只声明“我需要什么”,而不关心“如何获取”以及“由谁创建”。
- 极高可测试性:在测试环境下,可以直接通过容器注入虚假依赖(Stub/Mock)。
- 生命周期感知:通过作用域注解(Scope),精准控制对象在 Application、Activity 或 ViewModel 级别的存续时间。
三、 Android 依赖注入框架概览
| 框架 | 实现机制 | 核心特点 |
|---|---|---|
| Dagger 2 | 编译时生成 | 纯静态图谱,极致性能(纯 Java 代码调用,无反射),但配置极其繁琐。 |
| Koin | 运行时查找 | 纯 Kotlin DSL,无注解处理器,基于 Service Locator 模式,但错误在运行时才发现。 |
| Hilt | 编译时生成 + 字节码插桩 | Google 官方封装,保留了 Dagger 的极速性能,并抹平了 Android 生命周期组件的注入门槛。 |
四、 Hilt 实现
Hilt 的强大源于它在编译阶段对代码的三重加工。它并非通过反射寻找依赖,而是通过静态织入的方式,将依赖关系硬编码进字节码。我们以在 MainActivity中注入NetworkClient为例:
1. 编译期:工厂与注入器的自动化生成 (APT/KSP)
当你给类加上 @Inject 时,Hilt 的注解处理器会并行生成两类关键的 Java 代码:
依赖工厂 (_Factory.java)
Hilt 会为每个可注入的类生成一个实现了 Provider 接口的工厂类。它负责纯粹的“对象创建”。
// Hilt 自动生成的 NetworkClient_Factory.java
public final class NetworkClient_Factory implements Factory<NetworkClient> {
@Override
public NetworkClient get() {
return newInstance();
}
// 真正的实例化逻辑在这里,如果有其他依赖,会作为参数传进来
public static NetworkClient newInstance() {
return new NetworkClient();
}
}
成员注入器 (_MembersInjector.java)
针对 Activity/Fragment 等无法通过构造函数注入的组件,Hilt 生成了一个专门负责“赋值”的工具类。
// Hilt 自动生成的 MainActivity_MembersInjector.java
public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> {
// 提供给外部 Dagger 容器调用的静态方法
public static void injectNetworkClient(MainActivity instance, NetworkClient networkClient) {
instance.networkClient = networkClient; // 直接对目标类的 public/protected/包可见 变量进行赋值
}
}
2. 变换期:字节码插桩与动态继承 (ASM Bytecode Manipulation)
这是 Hilt 解决 Android 系统组件注入的核心黑科技。由于 Activity 是由系统通过反射实例化的(我们无法 new MainActivity()),因此无法拦截其构造过程。
当你写下 @AndroidEntryPoint class MainActivity 时,Hilt 的 Gradle 插件会在编译后的 .class字节码层面动刀:
// ASM 插入的抽象父类,实现了组件管理器接口
abstract class Hilt_MainActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {
private volatile ActivityComponentManager componentManager;
private boolean injected = false;
Hilt_MainActivity() {
super();
// 核心!利用 androidx 的生命周期回调,在 onCreate 真正执行前触发注入
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(Context context) {
inject();
}
});
}
protected void inject() {
if (!injected) {
injected = true;
// 1. 获取生成的 Dagger Component (依赖图谱大管家)
// 2. 将当前的 Activity 实例 (this) 投递进去进行装配
((MainActivity_GeneratedInjector) this.generatedComponent())
.injectMainActivity((MainActivity) this);
}
}
}
然后会执行一个名为 “Superclass Swap” (父类置换)的操作。
- 原始代码(你写的):
class MainActivity extends AppCompatActivity { ... } - 插桩后字节码(打包进 APK 的):
class MainActivity extends Hilt_MainActivity { ... }
Hilt 插件会修改 MainActivity.class 的类定义,将其 super_class 指针从原来的 androidx.appcompat.app.AppCompatActivity更改为 Hilt 自动生成的 com.example.Hilt_MainActivity。
3. 运行期:Component 的组装与最终赋值
Hilt 在编译期会把所有的模块(Module)和依赖汇总,生成一个上帝类(通常名为 DaggerMyApplication_HiltComponents_SingletonC)。这个类内部嵌套了层级结构,严格对应Android 的生命周期。当启动 MainActivity时:
- 系统实例化 MainActivity(虽然它现在继承自Hilt_MainActivity,但类名没变)
- 触发 Hilt_MainActivity 的构造函数:注册OnContextAvailableListener监听器
- 系统回调 onContextAvailable(在super.onCreate之前触发):
- 执行 Hilt_MainActivity.inject()方法。
- Hilt_MainActivity调用inject() 时,最终会流转到这个上帝类的内部实现:
public final class DaggerMyApplication_HiltComponents_SingletonC {
// 内部类,代表 Activity 级别的容器
private static final class ActivityCImpl extends ActivityComponent {
private final MainActivity mainActivity;
// 这里实现了 MainActivity_GeneratedInjector 接口
@Override
public void injectMainActivity(MainActivity arg0) {
injectMainActivity2(arg0);
}
private MainActivity injectMainActivity2(MainActivity instance) {
// 1. 调用 Factory 生成 NetworkClient 实例
// 2. 调用 MembersInjector 把实例赋值给 MainActivity 的字段
MainActivity_MembersInjector.injectNetworkClient(
instance,
NetworkClient_Factory.newInstance() // 如果有 @Scope,这里会从缓存里拿
);
return instance;
}
}
}
五、 总结
Hilt 的底层逻辑可以凝练为: “用编译时的静态发酵,换取运行时的极致纯粹” 。
- APT/KSP 解决了“怎么造对象”的问题(生成 Factory)。
- 字节码插桩(ASM) 解决了“什么时候把对象塞进去”的问题(生成 Hilt_Activity 并在生命周期前端拦截)。
- Component 嵌套类 实现了 的无反射依赖查找,并控制了对象的存活周期。