依赖注入:什么是依赖注入,iOS 和 Android 中的依赖注入

作者: IT Sectr 发布日期: 2026-02-18 阅读时间: 9 分钟

依赖注入(DI,Dependency Injection)—— 一种技术,对象从外部获取其依赖,而不是自行创建。DI 是 IoC(控制反转)原则的实现,也是 Dagger、Hilt 和 Swinject 的基础。依赖注入减少了代码耦合,简化了测试,并使架构更加灵活。在 Android 中,DI 的标准是通过 Google 的 Dagger Hilt,在 iOS 中 —— 通过 Swinject 或手动注入。更多信息 —— 参见 Android DI Guide

要点

  • 依赖注入 —— 依赖从外部传递给对象,而非在内部创建
  • 控制反转 —— DI 实现了 IoC 原则,流程控制移交给容器
  • Dagger Hilt —— Android 的 DI 标准,基于 Google 的 Dagger
  • Swinject —— 适用于 iOS 和 Swift 的流行 DI 框架
  • 降低耦合 —— 类依赖于抽象,而非具体实现

什么是依赖注入:DI 的本质和类型

依赖注入 —— 一种技术,对象通过构造函数、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:通过代码生成为 Android 提供 DI

Dagger Hilt —— 位于 Google Dagger 之上的层,是 Android 的标准 DI 库。Hilt 简化了 Dagger:消除了手动创建组件,添加了 @HiltAndroidApp、@AndroidEntryPoint 和 @Module。Hilt 与 Android 生命周期集成:ViewModel、Activity、Fragment、Service、BroadcastReceiver 可以通过注解接收依赖。代码生成发生在编译阶段 —— Dagger 生成组件的实现,从而实现零运行时开销。

kotlin
// 应用类
@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:在 Swift 中为 iOS 提供 DI

Swinject —— 适用于 iOS 的流行开源 DI 框架。Swinject 提供 Container、Assemblies 和各种作用域。与 Dagger 不同,Swinject 在运行时工作 —— 依赖无需代码生成即可动态解析。这使得 Swinject 配置更简单,但调试更困难:未解析的依赖错误仅在运行时出现。Swinject 支持构造函数注入、属性注入和方法注入。

swift
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 与 Service Locator 和手动注入的对比

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 个依赖。

kotlin
// ❌ 不好: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 创建,为什么还需要 DI?

new 在类之间创建了紧密耦合 —— 您无法在不更改代码的情况下替换实现。测试困难:无法将 mock 放入代替真实服务。违反了 SRP:类既负责业务逻辑,又负责依赖创建。DI 通过从外部注入依赖和使用抽象来解决这些问题。

Dagger Hilt 还是 Koin —— Android 该选择哪个?

Dagger Hilt —— Google 的标准,编译时 DI 带代码生成,性能更好且与 Jetpack 集成。Koin —— 运行时 DI,配置更简单,但速度较慢且有运行时错误。生产项目选择 Hilt。Koin 适用于原型和小型应用程序。

Swinject 是 iOS 唯一的 DI 吗?

不是。iOS 可用的有:Swinject(运行时,流行)、Needle(Uber 的编译时)、Dip(轻量级)、Weaver(基于 Sourcery)。Apple 不提供内置 DI 容器,但通过 init 手动注入是标准实践。对于 SwiftUI,通常通过 Environment 或 @StateObject 进行手动 DI 就足够了,无需外部库。

可以在没有框架的情况下使用 DI 吗?

可以。通过构造函数手动注入 —— 这是没有框架的 DI。Service Locator —— 没有框架的替代方案。工厂和工厂方法 —— 也是 DI 的一种形式。框架(Dagger、Swinject)自动化常规注册和依赖解析,但对于 10-20 个类,手动 DI 就足够了。

DI 是模式还是原则?

DI —— 是一种实现控制反转原则的技术(模式)。与 GoF 模式不同,DI 没有严格的 3-4 类结构。DI 是组织依赖的方式,而不是设计模式。DI 容器(Dagger、Swinject)是自动化这种技术的框架。

总结

  • DI —— 通过构造函数、setter 或方法从外部注入依赖的技术
  • Dagger Hilt —— Android 的 DI 标准,具有编译时代码生成和 @HiltViewModel
  • Swinject —— 适用于 iOS 的运行时 DI,具有 Container、Assembly 和作用域
  • 构造函数注入 —— 必需依赖的首选方式
  • 作用域 —— 无状态服务使用单例,屏幕依赖使用功能作用域
  • 测试 —— DI 简化了依赖替换为 mock 的过程,无需更改代码

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读