iOS 依赖注入库 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 语言级别支持的依赖注入方法包括以下三种:
- 构造函数注入
- 方法注入
- 属性注入
- 接口注入
- Service Locator
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对象,使用对象的时候直接调用。
@Injected是HomeViewController对象初始化完成就注入了,还有一个@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 类对象上,这些才是我们平常说的依赖。
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()