Skip to content
Wen's Blog

iOS 依赖注入库 Resolver 入门

Jan 11, 2022 — iOS

为什么需要依赖注入,则不在本文讨论范围中。

Why Resolver?

Swift 的依赖注入库有很多,比如 GitHub Star 数最多的Swinject (opens in a new window),也有大厂开源的如 Uber 的Needle (opens in a new window),百度的CarbonGraph (opens in a new window), 其它的如Cleanse (opens in a new window),以及本文介绍的Resolver (opens in a new window)

为什么技术选型时选择 Resolver? 最重要的一点是 Resolver 的接口设计非常简洁,能够适应各种依赖注入的场景。

作者 Michael Long 因此 Resolver 项目获得了 Google 颁发的 2021 年度Open Source Peer Bonus (opens in a new window)奖项。

简单介绍一下这个奖,这是由 Google 内部员工提名,给一些知名的开源项目作者或者核心贡献者的奖项,包括但不限于 Google 自己的开源项目。2021 年度获奖的作者中大部分是 Google 自己的开源项目的贡献者,比如 TensorFlow、Flutter、Go 等。也有些另类的贡献者,如 CocoaPods 的 Orta,以及本文介绍的 Resolver 项目作者。

基本用法

Swift 语言级别支持的依赖注入方法包括以下三种:

Resolver (opens in a new window) 完全支持上面几种方法,且是线程安全的。

基本的用法如下:

属性

class MyViewController: UIViewController {
var xyz: XYZViewModel = Resolver.resolve()
}

协议 任何对象都可以实现Resolving协议,只要实现这个协议,就有了一个默认的 resolver 变量。

class MyViewController: UIViewController, Resolving {
lazy var viewModel: XYZViewModel = resolver.resolve()
}
class ABCExample: Resolving {
lazy var service: ABCService = resolver.resolve()
}

接口注入 如果你是个依赖注入的强迫症患者,喜欢接口注入这种比较纯粹的方法,可以像下面一样:

class MyViewController: UIViewController {
lazy var viewModel = makeViewModel()
}
extension MyViewController: Resolving {
func makeViewModel() -> XYZViewModel { return resolver.resolve() }
}

当然也可以通过一个 readonly 的属性来达到目的:

extension MyViewController: Resolving {
var myViewModel: XYZViewModel { return resolver.resolve() }
}

这种方法有个缺点是每次调用都生成一个新的 ViewModel,可能不是你想要的。

可选注入 当有些依赖对象是可选时,Resolver 使用optional()方法返回可选对象。

var abc: ABCService? = resolver.optional()
var xyz: XYZService! = resolver.optional()

这里就得指定变量的类型了,否则 Swift 的类型推断会报错。

注解注入

2019 年 Swift 5.1 带来了的新特性:Property Wrappers (opens in a new window),这使得 Resolver 有了更简便的注入方式,通过@Injected注解来达到依赖注入。

下面我们来看一个简单的注解使用:

class HomeViewController: UIViewController {
@Injected var network: NetworkService
@LazyInjected var storage: StorageService
override void viewDidLoad() {
super.viewDidLoad()
let userId = storage.readUser().userId
network.load(with: userId)
...
}
}

@Injected使用非常简单,只需要在你的属性前面添加这个注解即可,无需手动初始化network对象,使用对象的时候直接调用。

@InjectedHomeViewController对象初始化完成就注入了,还有一个@LazyInjected注解,只有在真实调用的时候才会注入。

当然,上面的代码还不能直接使用,因为还没有注册注入对象。

注册服务

添加一个文件AppDelegate+Injection.swift,在里面粘贴如下代码:

extension Resolver: ResolverRegistering {
public static func registerAllServices() {
register { NetworkService() }
register { StorageService() }
}
}

扩展 Resolver,遵循ResolverRegistering协议,然后实现静态方法 registerAllServices(),在函数体内调用register{}注册所有需要的依赖对象。

Resolver 会利用 Swift 的类型推断来自动决定和注册返回值的类型,当然你也可以特别指定你的对象实现了哪些协议。

register { XYZCombinedService() }
.implements(XYZFetching.self)
.implements(XYZUpdating.self)

当你使用面向协议编程时,变量类型会更多的使用协议,而不是实现协议的类,如

protocol NetworkService {
func load() -> [Any]
}
class NetworkServiceImpl: NetworkService {
func load() -> [Any] {
...
return []
}
}
class HomeViewController {
@Injected var network: NetworkService
}

这时,除了类似上面.implements()方法,还可以使用as 方法:

main.register { NetworkServiceImpl() as NetworkService }

协议还可以共享同一个实例,如XYZCombinedService对象初始化之后,后面的resolve()操作都是直接返回前面已经初始化的对象:

main.register { resolve() as XYZCombinedService as XYZFetching }
main.register { resolve() as XYZCombinedService as XYZUpdating }
main.register { XYZCombinedService() }

带参数的依赖注入

有时候,依赖对象初始化时,需要额外的参数。Resolver 提供了一个参数args,可以往args添加你想要的参数,这是一个 Swift 5.2 的callAsFunction (opens in a new window)的功能实现。

例如一个NetworkService需要区分测试环境和正式环境,它们的baseURL不一样。

我们可以这么做:

class ViewController: UIViewController, Resolving {
var baseURL = "https://test.google.com"
lazy var viewModel: NetworkService = resolver.resolve(args: baseURL)
}

注册的时候将 args 参数传递给构造函数即可:

register { (_, args) in
NetworkService(baseURL: args())
}

多个参数传递

可以将args当做 Map 传递,实际 Resolver 接收到的是 [String:Any?]类型。

class ViewController: UIViewController, Resolving {
lazy var viewModel: XYZViewModel = resolver.resolve(args: ["mode": true, "name": "Editing")
}
register { (_, args) in
XYZViewModel(editMode: args("mode"), name: args("name"))
}

可选参数传递

register { (_, args) in
XYZService(someOptionalValue: args.optional())
}

当你使用@Injected的时候,参数不可用,没法做到下面这样:

@Injected(args: baseURL) var network: NetworkService

虽然 Resolver 可以解决带参数的依赖注入,但是不太建议这么做,因为数据不适合用来注入,比如详情界面跳转携带的userId等。大家要更关注在 Service 类对象上,这些才是我们平常说的依赖。

Scopes

Resolver 提出了一个 Scopes 概念,简单来说是控制一个依赖对象的生命周期。

大部分人习惯了单例,任何一个多个地方需要用到的对象,都可以用单例实现,避免重复生成对象。目的是没有错的,但是却用错了方法。

Resolver 建议我们使用 Scopes 来严格控制依赖对象的创建和销毁,有些依赖对象只存在于一个 ViewController,有的是应用级别的,有的不用了可以销毁。

Resolver 内置了 6 种 scopes:

具体用法

NetworkService 在整个 Application 生命周期中都唯一存在。

register { NetworkService() }
.scope(.application)

自定义缓存

extension ResolverScope {
static let session = ResolverScopeCache()
}

注册时指定.session

register { NetworkService() }
.scope(.session)

需要的时候进行重置:

ResolverScope.session.reset()

参考