7 min read

iOS 依赖注入库 Resolver 入门

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

Why Resolver?

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

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

作者 Michael Long 因此 Resolver 项目获得了 Google 颁发的 2021 年度Open Source Peer Bonus奖项。

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

基本用法

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

  • 构造函数注入
  • 方法注入
  • 属性注入
  • 接口注入
  • Service Locator

Resolver 完全支持上面几种方法,且是线程安全的。

基本的用法如下:

属性

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,这使得 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的功能实现。

例如一个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:

  • .application

    在 App 运行期间,Resolver 会一直持有对象,且只在调用时初始化一次,每次都返回初始化的值。

  • .cached::

    Resolver 会一直持有对象,且只在调用时初始化一次,每次都返回初始化的值。和.application不同的是,.cached可以重置所有对象,通过调用ResolverScope.cached.reset()释放掉所有持有的缓存依赖对象。

  • .graph

    默认的 scopes,内部有一个决策循环,会重用已经创建的依赖对象。假如 A 依赖 B、C,B 和 C 都依赖 D,注册 A 对象时,会先注册 B 中的 D,到注册 C 时就不会重复注册 D 了,会重用 B 之前的 D。

  • .shared

    弱引用对象,只适用于 class 类型,不适用 value 类型。当有一个强引用持有resolve()的对象,后续的调用都会返回同一个对象。当所有强引用释放掉了之后,.shared 的实例也会释放,直到下一次调用resolve(),这时会创建一个新的实例。

  • .unique

    唯一的依赖对象,每次调用resolve()都会返回新的实例。

  • .container

    有点类似.cached,在这个 scope 里面的对象会被缓存,直到缓存被重置或释放,或者 containner 不再存在。常用使用场景是用于依赖对象跟容器的生命周期一致的情况,比如在运行期手动创建 mock 和 testing,用完之后就释放掉。

具体用法

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

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

自定义缓存

extension ResolverScope {
    static let session = ResolverScopeCache()
}

注册时指定.session

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

需要的时候进行重置:

ResolverScope.session.reset()

参考