目录
  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
打赏
  • 微信
  • 支付宝

评论
简