目录
  1. 1. 一、控制反转与依赖注入的概念
    1. 1.1. 1.1 DRY 原则下的 DI 价值
    2. 1.2. 1.2 DI 与 Service Locator 的区别
  2. 2. 二、三种注入方式
    1. 2.1. 2.1 构造函数注入(推荐方式)
    2. 2.2. 2.2 字段注入
    3. 2.3. 2.3 方法注入
  3. 3. 三、编译期 DI:Dagger/Hilt 原理
    1. 3.1. 3.1 核心概念
    2. 3.2. 3.2 Component 的实现原理:生成代码解析
    3. 3.3. 3.3 Subcomponent(子组件)
    4. 3.4. 3.4 Binds 与 Provides 的区别
    5. 3.5. 3.5 Dagger 的编译期验证
  4. 4. 四、Hilt:Dagger 的官方 Android 封装
    1. 4.1. 4.1 预定义 Component 层级
    2. 4.2. 4.2 @AndroidEntryPoint 自动注入
    3. 4.3. 4.3 @HiltViewModel 与 Jetpack 集成
    4. 4.4. 4.4 @ViewModelInject 与 SavedStateHandle
  5. 5. 五、Dagger/Hilt 的构建速度挑战
    1. 5.1. 5.1 KAPT vs KSP
    2. 5.2. 5.2 模块化项目的 Dagger 配置
  6. 6. 六、运行时 DI:Koin
  7. 7. 七、Dagger vs Koin vs Hilt
  8. 8. 八、面试常问题目
解读开源框架系列-IOC架构设计

一、控制反转与依赖注入的概念

控制反转(IoC, Inversion of Control)是一种设计原则,最通俗的理解是:不要让类自己创建它所依赖的对象,而是由外部(容器/框架)注入进来。依赖注入(DI, Dependency Injection)是 IoC 的一种具体实现方式。

1.1 DRY 原则下的 DI 价值

在传统写法中,类自己控制依赖的创建:

public class NewsRepository {
private NewsApi newsApi;

public NewsRepository() {
// 类自己创建依赖——硬编码、不可测试
this.newsApi = new Retrofit.Builder()
.baseUrl("https://api.example.com")
.build()
.create(NewsApi.class);
}
}

使用依赖注入后:

public class NewsRepository {
private final NewsApi newsApi;

// 依赖由外部传入——可测试、可替换
@Inject
public NewsRepository(NewsApi newsApi) {
this.newsApi = newsApi;
}
}

依赖注入的三个核心好处:

  1. 可测试性:可以在测试中传入 mock 对象。
  2. 松耦合:NewsRepository 不需要知道 NewsApi 是如何创建的。
  3. 可替换性:更换实现只需修改注入配置,无需改动依赖方代码。
  4. 集中管理:对象的创建和生命周期管理集中在 Module 中,符合 DRY(Don’t Repeat Yourself)原则。

1.2 DI 与 Service Locator 的区别

很多人将依赖注入与服务定位器混为一谈:

// Service Locator 模式(主动拉取)
public class NewsViewModel {
private NewsRepository repo = ServiceLocator.get(NewsRepository.class);
}

// 依赖注入模式(被动注入)
public class NewsViewModel {
private final NewsRepository repo;

@Inject
public NewsViewModel(NewsRepository repo) {
this.repo = repo;
}
}

关键区别:

  • Service Locator:类主动向全局容器请求依赖。缺点:依赖关系不透明(不知道 NewsViewModel 需要哪些依赖),测试时必须同时 mock ServiceLocator。
  • Dependency Injection:依赖被动注入。优点:依赖关系在构造函数中清晰可见,测试时直接传入 mock 对象即可。

二、三种注入方式

2.1 构造函数注入(推荐方式)

public class UserViewModel {
private final UserRepository repository;

@Inject // Dagger 通过此注解标记构造函数
public UserViewModel(UserRepository repository) {
this.repository = repository;
}
}

构造函数注入的优势是:(1) 依赖在对象创建时就确定,不会出现 NPE;(2) 可以将依赖声明为 final;(3) 便于发现循环依赖——如果 A 依赖 B 且 B 依赖 A,编译时会检测到。

2.2 字段注入

public class MainActivity extends AppCompatActivity {
@Inject
UserViewModel viewModel; // 不可声明为 final

@Override
protected void onCreate(Bundle savedInstanceState) {
// 需要手动触发注入
((MyApplication) getApplication()).getAppComponent().inject(this);
super.onCreate(savedInstanceState);
}
}

字段注入的缺点:(1) 依赖不可 final;(2) 容易出现忘记触发注入的情况;(3) 在单元测试中难以替换依赖。Android 中主要用于 Activity/Fragment 等不能控制构造函数创建的对象。

2.3 方法注入

public class MainActivity extends AppCompatActivity {
private UserViewModel viewModel;

@Inject
public void setViewModel(UserViewModel viewModel) {
this.viewModel = viewModel;
}
}

方法注入使用较少,主要用于向后兼容(已经有 setter)或需要对注入的依赖做额外处理。

三、编译期 DI:Dagger/Hilt 原理

Dagger(https://github.com/google/dagger)是 Android 生态中最主流的编译期 DI 框架,由 Google 维护。其核心原理是通过 APT(Annotation Processing Tool)在编译期生成工厂类,完全避免运行时的反射开销。

3.1 核心概念

Component(组件):依赖注入的容器和桥梁。定义哪些模块提供什么依赖,注入给哪些目标。

@Component(modules = {NetworkModule.class, DatabaseModule.class})
@Singleton // Component 的作用域
public interface AppComponent {
// 暴露的依赖(供应点)
NewsApi getNewsApi();
UserDao getUserDao();

// 注入目标
void inject(MainActivity activity);
}

Module(模块):提供对象创建逻辑的地方,尤其是对第三方库(Retrofit、Room)的实例化。

@Module
public class NetworkModule {
@Provides
@Singleton // 与方法作用域对应
public NewsApi provideNewsApi() {
return new Retrofit.Builder()
.baseUrl("https://api.example.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NewsApi.class);
}
}

Scope(作用域):控制依赖的生命周期。@Singleton 是内置的作用域,此外可自定义:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface FragmentScope {}

作用域确保:同一个 Component 实例中,被同一作用域标记的依赖只会创建一次。

Qualifier(限定符):区分同类型的多个依赖实例。

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface LoggingInterceptor {}

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthInterceptor {}

@Module
public class NetworkModule {
@Provides @LoggingInterceptor
public Interceptor provideLoggingInterceptor() {
return new HttpLoggingInterceptor();
}

@Provides @AuthInterceptor
public Interceptor provideAuthInterceptor() {
return chain -> chain.proceed(
chain.request().newBuilder()
.addHeader("Authorization", "Bearer " + token)
.build()
);
}
}

3.2 Component 的实现原理:生成代码解析

Dagger 为每个 @Component 注解的接口生成一个 DaggerXXXComponent 实现类。以下是实际的生成代码解析:

// 生成的 DaggerAppComponent.java(简化)
public final class DaggerAppComponent implements AppComponent {
private final NetworkModule networkModule;

private DaggerAppComponent(NetworkModule networkModuleParam) {
this.networkModule = networkModuleParam;
}

// 工厂方法
public static Builder builder() {
return new Builder();
}

@Override
public NewsApi getNewsApi() {
// 通过生成的 Factory 类获取实例
return NetworkModule_ProvideNewsApiFactory.provideNewsApi(networkModule);
}

@Override
public void inject(MainActivity activity) {
injectMainActivity(activity);
}

private MainActivity injectMainActivity(MainActivity instance) {
// 注入 ViewModel(如果 ViewModel 依赖也由 Dagger 管理)
MainActivity_MembersInjector.injectViewModel(instance,
new UserViewModel(DataModule_ProvideNewsApiFactory.provideNewsApi(networkModule)));
return instance;
}

public static final class Builder {
private NetworkModule networkModule;

public Builder networkModule(NetworkModule module) {
this.networkModule = Objects.requireNonNull(module);
return this;
}

public AppComponent build() {
if (networkModule == null) {
this.networkModule = new NetworkModule(); // 如果没有显式提供,使用默认构造
}
return new DaggerAppComponent(networkModule);
}
}
}

Double Check 模式在 Scoped Provider 中的应用

作用域绑定时,Dagger 生成的 Provider 使用 Double Check 确保线程安全:

// 生成的有 Scoped Provider(如 @Singleton)
public final class NewsApiProvider implements Provider<NewsApi> {
private final NetworkModule module;
private volatile NewsApi instance;

@Override
public NewsApi get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = NetworkModule_ProvideNewsApiFactory.provideNewsApi(module);
}
}
}
return instance;
}
}

Dagger 生成的每个 @Provides 方法对应一个 Factory 类:

// 生成的 NetworkModule_ProvideNewsApiFactory.java
public final class NetworkModule_ProvideNewsApiFactory implements Factory<NewsApi> {
private final NetworkModule module;

public NetworkModule_ProvideNewsApiFactory(NetworkModule module) {
this.module = module;
}

@Override
public NewsApi get() {
return provideNewsApi(module);
}

public static NewsApi provideNewsApi(NetworkModule instance) {
return Preconditions.checkNotNull(
instance.provideNewsApi(),
"Cannot return null from a non-nullable @Provides method"
);
}

public static NetworkModule_ProvideNewsApiFactory create(NetworkModule module) {
return new NetworkModule_ProvideNewsApiFactory(module);
}
}

3.3 Subcomponent(子组件)

Subcomponent 用于将 Component 的生命周期与 Activity/Fragment 绑定:

@ActivityScope
@Subcomponent(modules = {ActivityModule.class})
public interface ActivityComponent {
void inject(MainActivity activity);

@Subcomponent.Factory
interface Factory {
ActivityComponent create(@BindsInstance Context context);
}
}

Subcomponent 继承了父 Component 的所有依赖,同时有自己的作用域和生命周期。当 Activity 销毁时,Subcomponent 也被释放,其管理的依赖随之被 GC。

Subcomponent 的源码生成:Dagger 为 Subcomponent 生成的实现类实现了父 Component 的所有 provision 方法,并额外实现了 Subcomponent 自身的 Module 方法。关键点在于:父 Component 通过返回 Subcomponent.Factory 的 bound instance 方法将运行时上下文传递进去。

3.4 Binds 与 Provides 的区别

@Module
public abstract class DatabaseModule {
// @Binds:用于接口绑定实现(必须是 abstract 方法)
@Binds
abstract UserDataSource bindUserDataSource(LocalUserDataSource impl);

// @Provides:用于需要 new 或涉及复杂创建逻辑的对象
@Provides
@Singleton
public AppDatabase provideAppDatabase(Context context) {
return Room.databaseBuilder(context, AppDatabase.class, "app.db").build();
}
}

@Binds 比 @Provides 更高效的原因:Dagger 可以直接使用实现类的 @Inject 构造函数或生成 Provider,不需要实际调用一个方法体。对于 @Provides,生成的代码必须调用 module 的 provides 方法。

3.5 Dagger 的编译期验证

Dagger 在编译期验证依赖图的完整性和正确性,这是其相对于运行时 DI 框架(如 Koin、Guice)的最大优势:

  1. 缺失绑定检测:如果某个类需要注入但没有任何 Module 提供,编译失败。
  2. 循环依赖检测A(B)B(A) 的构造注入会直接报编译错误。
  3. 作用域一致性:如果一个 unscoped 的依赖被注入到 scoped 的 Component 中,编译警告(通过 Dagger 的 @Component.Builder 的 bound instance 可以绕过,但显式声明了意图)。
  4. Nullable 正确性@Nullable 注入与 null 检查的一致性验证。

四、Hilt:Dagger 的官方 Android 封装

Hilt 在 Dagger 基础上的关键增强:

4.1 预定义 Component 层级

SingletonComponent  (@Singleton)

ViewModelComponent (@ViewModelScoped)

ActivityComponent (@ActivityScoped)

FragmentComponent (@FragmentScoped)

ViewComponent (@ViewScoped)

4.2 @AndroidEntryPoint 自动注入

@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
@Inject
NewsViewModel viewModel; // Hilt 自动调用注入

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // 在 super.onCreate() 时完成注入
viewModel.loadNews();
}
}

Hilt 通过 Gradle Transform 为 @AndroidEntryPoint 修改基类,在 onCreate 前执行注入逻辑。具体是在字节码层面为 Activity 插入 Hilt_MainActivity 作为中间父类。

4.3 @HiltViewModel 与 Jetpack 集成

@HiltViewModel
public class NewsViewModel @Inject constructor(
private val repository: NewsRepository
) : ViewModel() {
// Hilt 自动将 @HiltViewModel 的 ViewModel 绑定到 ViewModelComponent
}

Hilt 使用 ViewModelComponent 管理 ViewModel 的依赖生命周期,确保 ViewModel 在配置变更后存活。

4.4 @ViewModelInject 与 SavedStateHandle

@HiltViewModel
public class DetailViewModel @Inject constructor(
private val repository: NewsRepository,
@Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val newsId: String = savedStateHandle.get<String>("newsId") ?: ""
}

Hilt 使用 @Assisted 区分”由 DI 框架提供的依赖”和”由外部提供的依赖”。SavedStateHandle 是后者——由 ViewModel 框架在创建时提供,不同 ViewModel 实例有不同的 handle。

五、Dagger/Hilt 的构建速度挑战

5.1 KAPT vs KSP

KAPT(Kotlin Annotation Processing Tool)会生成 Java stub 文件,导致编译时间显著增加。KSP(Kotlin Symbol Processing)直接处理 Kotlin 符号,速度快 2-3 倍。

Dagger 2.37+ 已开始实验性支持 KSP。迁移方式:

// 将 kapt 替换为 ksp
plugins {
id("com.google.devtools.ksp") version "1.9.0-1.0.0"
}

dependencies {
ksp("com.google.dagger:dagger-compiler:2.46")
}

5.2 模块化项目的 Dagger 配置

大型项目中,为每个模块创建独立的 Component/Module 会增加编译负担。推荐使用:

  • @Module(includes = ...) 组合多个 Module
  • @Component(dependencies = ...) 声明 Component 依赖关系(类似 Subcomponent,但 Component 之间互相独立,不继承 provision 方法)

六、运行时 DI:Koin

Koin(https://github.com/InsertKoinIO/koin)是 Kotlin 编写的轻量级 DI 框架,基于 Kotlin DSL 和函数式编程,无需 APT。

// Koin 模块定义
val appModule = module {
single { Retrofit.Builder().baseUrl("https://api.example.com").build().create(NewsApi::class.java) }
single { UserRepository(get()) } // get() 自动解析依赖
}

val viewModelModule = module {
viewModel { UserViewModel(get()) }
}

// Application 中初始化
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApp)
modules(appModule, viewModelModule)
}
}
}

// Activity 中使用
class MainActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModel()
}

Koin 通过 Kotlin 的 inline + reified 实现类型解析,在运行时通过映射表查找依赖。其优点是配置简洁,无需 kapt(编译速度更快);缺点是依赖在运行时解析,编译期无法发现依赖未绑定的错误。

七、Dagger vs Koin vs Hilt

特性 Dagger Koin Hilt
原理 编译期 APT 运行时 Service Locator 编译期 APT(封装 Dagger)
编译速度 慢(kapt) 快(无 kapt) 慢但封装简单
配置复杂度 低(自动化)
错误发现 编译期 运行时 编译期
Kotlin 支持 一般 原生 优良
性能 无反射开销 有轻微运行时开销 无反射开销
生产推荐 中大型项目 中小型/快速原型 官方推荐

八、面试常问题目

Q1: Dagger 的 @Component 和 @Subcomponent 有什么区别?

@Component 是独立的依赖容器,有自己的作用域和生命周期,通常对应 Application 级别。@Subcomponent 是 Component 的子容器,继承父 Component 的所有依赖,但有自己的作用域(如 @ActivityScope),生命周期与其宿主(如 Activity)绑定。当 Activity 销毁时,Subcomponent 被释放,其管理的 @ActivityScope 依赖也随之释放,防止内存泄漏。

Q2: Dagger 如何解决循环依赖问题?

当 A 的构造函数依赖 B,B 的构造函数依赖 A 时,形成循环依赖。Dagger 会在编译期检测到这种循环并报错 “Found a dependency cycle”。解决方法是:将其中一个依赖改为 Provider<T> 或 Lazy<T> 延迟注入,或者引入第三个类 C 打破循环,或者将其中一个注入方式从构造函数注入改为字段注入。

Q3: @Binds 和 @Provides 有什么不同?为什么 @Binds 方法必须是 abstract 的?

@Binds 用于接口到实现的绑定,方法必须是 abstract 的且只有一个参数(实现类型),Dagger 直接知道”当请求接口时,返回这个实现”即可,无需方法体。@Provides 用于需要 new 对象或进行复杂创建逻辑的场景,方法必须有方法体。@Binds 比 @Provides 更高效,因为 Dagger 不需要调用方法体,直接使用实现类的构造函数。

Q4: Hilt 相对 Dagger 做了哪些简化?

Hilt 自动生成了 Component 层级(SingletonComponent → ViewModelComponent → ActivityComponent → FragmentComponent 等),无需手动定义 @Subcomponent;自动注入 Android 类(@AndroidEntryPoint 标记 Activity/Fragment 等),无需手动调用 inject();预定义 @ApplicationContext 和 @ActivityContext 限定符;集成 Jetpack ViewModel,一行 @HiltViewModel 即可。

Q5: 为什么依赖注入框架对测试如此重要?

依赖注入使单元测试的 Arrange 阶段变得简单:可以直接传入 mock 依赖而不需要修改源代码。在传统代码中,实例化一个 NewsViewModel 可能需要在内部创建 Retrofit、OkHttp、Gson、Room 等一系列对象,测试时必须准备完整的 Android 环境。DI 将依赖外置后,测试只需:val vm = NewsViewModel(mockRepo),极大降低了测试编写成本,也使得测试更聚焦于被测单元本身。


参考源码路径:

  • Dagger:https://github.com/google/dagger
  • Dagger APT 核心:dagger-compiler/src/main/java/dagger/internal/codegen/ComponentProcessor.java
  • Hilt:https://dagger.dev/hilt/
  • Hilt Gradle Plugin:hilt-android-gradle-plugin/src/main/kotlin/dagger/hilt/android/plugin
  • Koin:https://github.com/InsertKoinIO/koin
  • Guice(Google 的运行时 DI):https://github.com/google/guice
  • KSP:https://github.com/google/ksp
打赏
  • 微信
  • 支付宝

评论