依赖注入(DI,Dependency Injection)—— 一种技术,对象从外部获取其依赖,而不是自行创建。DI 是 IoC(控制反转)原则的实现,也是 Dagger、Hilt 和 Swinject 的基础。依赖注入减少了代码耦合,简化了测试,并使架构更加灵活。在 Android 中,DI 的标准是通过 Google 的 Dagger Hilt,在 iOS 中 —— 通过 Swinject 或手动注入。更多信息 —— 参见 Android DI Guide。
要点
依赖注入 —— 一种技术,对象通过构造函数、setter 或接口接收依赖(服务、存储库、配置),而不是自己用 new 创建它们。DI 的目标是减少类之间的耦合。如果类自己创建依赖,它就会与具体实现紧密绑定,从而使测试和修改变得困难。在 DI 中,类与抽象(protocol/interface)协作,而具体实现由外部提供。
三种注入方式 —— 构造函数注入(通过 init/constructor)、Setter 注入(通过属性/setter)、接口注入(通过接口方法)。构造函数注入 —— 首选方式:依赖在签名中清晰可见,对象始终在有效状态下创建。Setter 注入用于带有默认值的可选依赖。接口注入 —— 很少使用,主要用于 DI 容器。
| DI 类型 | 方式 | 何时使用 | 示例 |
|---|---|---|---|
| 构造函数 | 初始化参数 | 必需依赖 | init(service: ServiceProtocol) |
| 属性 | 类属性 | 可选依赖 | var service: ServiceProtocol? |
| 方法 | 方法参数 | 临时依赖 | func doWork(with service: Service) |
DI 容器 —— 管理依赖创建和生命周期的库。容器包含类型注册(每个抽象类型映射到具体实现)以及用于创建具有已解析依赖的对象的工厂。在 Android 中 —— Dagger/Hilt,在 iOS 中 —— Swinject、Needle、Dip。容器可以管理作用域(Scope):单例(每个应用程序一个实例)、功能作用域(每个屏幕)或每次请求新对象。
Dagger Hilt —— 位于 Google Dagger 之上的层,是 Android 的标准 DI 库。Hilt 简化了 Dagger:消除了手动创建组件,添加了 @HiltAndroidApp、@AndroidEntryPoint 和 @Module。Hilt 与 Android 生命周期集成:ViewModel、Activity、Fragment、Service、BroadcastReceiver 可以通过注解接收依赖。代码生成发生在编译阶段 —— Dagger 生成组件的实现,从而实现零运行时开销。
// 应用类
@HiltAndroidApp
class MyApp : Application()
// 模块 —— 定义如何创建依赖
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()
@Provides
@Singleton
fun provideApiService(client: OkHttpClient): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(client)
.build()
.create(ApiService::class.java)
}
}
// ViewModel 通过构造函数接收依赖
@HiltViewModel
class MainViewModel @Inject constructor(
private val apiService: ApiService
) : ViewModel() {
private val _state = MutableStateFlow(MainState.Loading)
val state: StateFlow<MainState> = _state.asStateFlow()
fun loadData() {
viewModelScope.launch {
_state.value = MainState.Success(apiService.getData())
}
}
}
// Activity —— @AndroidEntryPoint 启用 DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
}
Dagger 组件和可见性范围 —— @Singleton(整个应用程序)、@ActivityScoped(Activity)、@FragmentScoped(Fragment)、@ViewModelScoped(ViewModel)。选择作用域决定了对象的生命周期。@Singleton —— 每个进程一个实例,适用于 OkHttpClient 和数据库。@ActivityScoped —— 对象在 Activity 存活期间存活,用于屏幕依赖。@ViewModelScoped —— Hilt 2.45+ 的新特性,对象在 ViewModel 存活期间存活,方便协程作用域。
Swinject —— 适用于 iOS 的流行开源 DI 框架。Swinject 提供 Container、Assemblies 和各种作用域。与 Dagger 不同,Swinject 在运行时工作 —— 依赖无需代码生成即可动态解析。这使得 Swinject 配置更简单,但调试更困难:未解析的依赖错误仅在运行时出现。Swinject 支持构造函数注入、属性注入和方法注入。
import Swinject
// Assembly —— 注册组
class NetworkAssembly: Assembly {
func assemble(container: Container) {
container.register(NetworkServiceProtocol.self) { _ in
NetworkService()
}.inObjectScope(.container) // singleton
container.register(UserRepositoryProtocol.self) { r in
UserRepository(
networkService: r.resolve(NetworkServiceProtocol.self)!
)
}
}
}
// ViewModel 通过构造函数注入
class ProfileViewModel: ObservableObject {
private let repository: UserRepositoryProtocol
init(repository: UserRepositoryProtocol) {
self.repository = repository
}
@Published var user: User?
func loadUser() {
repository.fetchUser { [weak self] user in
self?.user = user
}
}
}
// 在 AppDelegate 或 App 中配置 DI
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!
// UIKit ViewController 的属性注入
container.register(UserViewController.self) { r in
let vc = UserViewController()
vc.viewModel = r.resolve(ProfileViewModel.self)
return vc
}
Swinject 作用域 —— .transient(每次新对象)、.container(每个容器单例)、.graph(默认 —— 对象在单个依赖图内共享)。对于 iOS 应用程序,.container 和 .transient 就足够了。Swinject 还支持 Assembler —— 为模块化架构分组 Assembly。对于测试,Assembly 被 MockAssembly 替换,从而无需更改生产代码即可替换依赖。
DI vs Service Locator —— 两种模式都解决了依赖管理问题,但方式不同。DI 将依赖注入到对象中,Service Locator 提供一个全局注册表,对象从中自行请求依赖。DI 通过构造函数(或 setter)显式声明依赖。Service Locator 隐藏依赖 —— 它们在方法内部被请求,这使得签名信息量减少。DI 更容易测试:只需在构造函数中放入 mock。Service Locator 需要为每个测试配置全局注册表。
| 特性 | 依赖注入 | Service Locator | 手动注入 |
|---|---|---|---|
| 依赖的明确性 | 在构造函数中 | 隐藏在方法体中 | 显式 |
| 测试 | 构造函数中的 Mock | 配置 Locator | 构造函数中的 Mock |
| 配置复杂度 | 需要 DI 容器 | 全局注册表 | 手动创建 |
| 运行时开销 | Dagger —— 编译时 | 运行时查找 | 无 |
DI vs 手动注入 —— 没有 DI 容器,依赖在工厂或 AppDelegate 中手动创建。对于 5-10 个类,手动注入更简单 —— 不需要学习 Dagger 或 Swinject。对于 50+ 个类,手动注入成为问题:5-6 个参数的构造函数、复杂的创建顺序、代码重复。DI 容器自动化这些过程并提供清晰的 lifecycle。无容器的手动注入 —— 适合小型项目和原型的好选择。
构造函数注入 —— 标准。始终对必需依赖使用构造函数注入。这使依赖显式化,对象始终准备好工作。Setter 注入 —— 仅用于可选依赖(例如 delegate 或 listener)。接口注入 —— 除非编写自己的 DI 库,否则不要使用。构造函数注入 —— 确保对象在有效状态下创建的唯一方法。
一个类 —— 一个职责。如果类的构造函数需要 5+ 个参数,则该类可能违反了单一职责原则。将类拆分为多个具有更少依赖的类。标志:如果您正在编写具有 6 个不同服务的 ServiceManager 类 —— 这是 God Object 反模式。将业务逻辑提取到 Use Cases(Interactors)中,每个包含 1-2 个依赖。
// ❌ 不好:6 个依赖 —— God Object
class ProfileViewModel @Inject constructor(
private val api: ApiService,
private val db: Database,
private val analytics: Analytics,
private val prefs: Preferences,
private val location: LocationProvider,
private val notification: NotificationManager
)
// ✅ 好:具有 1-2 个依赖的 Use Cases
class ProfileViewModel @Inject constructor(
private val loadProfileUseCase: LoadProfileUseCase,
private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)
作用域和生命周期 —— 为每个依赖选择正确的作用域。单例:OkHttpClient、数据库、SharedPreferences。功能作用域:存储库、Use Cases(如果它们没有状态)。Transient:Value Objects、DateFormatter、解析器。作用域错误 —— 常见问题:存储屏幕状态的单例导致内存泄漏。在 Android Hilt 中,@ActivityScoped 解决了这个问题,在 Swinject 中 —— .container 需谨慎使用。
常见问题解答
new 在类之间创建了紧密耦合 —— 您无法在不更改代码的情况下替换实现。测试困难:无法将 mock 放入代替真实服务。违反了 SRP:类既负责业务逻辑,又负责依赖创建。DI 通过从外部注入依赖和使用抽象来解决这些问题。
Dagger Hilt —— Google 的标准,编译时 DI 带代码生成,性能更好且与 Jetpack 集成。Koin —— 运行时 DI,配置更简单,但速度较慢且有运行时错误。生产项目选择 Hilt。Koin 适用于原型和小型应用程序。
不是。iOS 可用的有:Swinject(运行时,流行)、Needle(Uber 的编译时)、Dip(轻量级)、Weaver(基于 Sourcery)。Apple 不提供内置 DI 容器,但通过 init 手动注入是标准实践。对于 SwiftUI,通常通过 Environment 或 @StateObject 进行手动 DI 就足够了,无需外部库。
可以。通过构造函数手动注入 —— 这是没有框架的 DI。Service Locator —— 没有框架的替代方案。工厂和工厂方法 —— 也是 DI 的一种形式。框架(Dagger、Swinject)自动化常规注册和依赖解析,但对于 10-20 个类,手动 DI 就足够了。
DI —— 是一种实现控制反转原则的技术(模式)。与 GoF 模式不同,DI 没有严格的 3-4 类结构。DI 是组织依赖的方式,而不是设计模式。DI 容器(Dagger、Swinject)是自动化这种技术的框架。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。