如何避免我的自定义依赖项注入工具出现保留周期?

How to avoid a retain cycle on my custom dependency injection tool?

我创建了一个自定义 propertyWrapper 来在代码中注入我的依赖项,因此在测试代码时,我可以使用 WritableKeyPath link 将模拟传递给对象内存。

这就是我在生产代码中使用它的方式。这非常方便,因为我不需要在初始化程序中传递对象。

@Injected(\.child) var child

这就是我在单元测试中使用它来代替 WritableKeyPath 通过模拟的方式。

let parentMock = ParentMock()
InjectedDependency[\.parent] = parentMock

问题是,在我尝试使用它的代码的某些部分,当 Child class 需要访问时,似乎正在创建幽灵对象到Parentclass一个循环。当我在 Playground 中查找并使用它时,我可能注意到当 linked 彼此时创建了两个对象,并且在设置变量为 nil.

如何更新我的 @propertyWrapper 或可以对此解决方案进行哪些改进以使其按预期工作?为什么创建两个对象而不是它们引用内存中的对象?

所以下面设置这个自定义依赖注入工具在代码中的使用。 我已经用 weak var parent: Parent? 实现了 classic 方法来释放内存中的对象,没有问题来展示我的预期。

protocol ParentProtocol {}
class Parent: ParentProtocol {

  //var child: Child?
  @Injected(\.child) var child

  init() { print(" Allocating Parent in memory") }
  deinit { print ("♻️ Deallocating Parent from memory") }
}

protocol ChildProtocol {}
class Child: ChildProtocol {

  //weak var parent: Parent?
  @Injected(\.parent) var parent

  init() { print(" Allocating Child in memory") }
  deinit { print("♻️ Deallocating Child from memory") }
}

var mary: Parent? = Parent()
var tom: Child? = Child()

mary?.child = tom!
tom?.parent = mary!

// When settings the Parent and Child to nil,
// both are expected to be deallocating.
mary = .none
tom = .none

这是使用自定义依赖注入解决方案时日志中的响应。

 Allocating Parent in memory
 Allocating Child in memory
 Allocating Child in memory // Does not appear when using the weak reference. 
♻️ Deallocating Child from memory
 Allocating Parent in memory // Does not appear when using the weak reference. 
♻️ Deallocating Parent from memory

这是我的自定义 PropertyWrapper 的实现,用于处理 ParentChild 键之后的依赖项注入作为使用示例。

// The key protocol for the @propertyWrapper initialization.
protocol InjectedKeyProtocol {
  associatedtype Value
  static var currentValue: Self.Value { get set }
}

// The main dependency injection custom tool.
@propertyWrapper
struct Injected<T> {

    private let keyPath: WritableKeyPath<InjectedDependency, T>

    var wrappedValue: T {
        get { InjectedDependency[keyPath] }
        set { InjectedDependency[keyPath] = newValue }
    }

    init(_ keyPath: WritableKeyPath<InjectedDependency, T>) {
        self.keyPath = keyPath
    }
}

// The custom tool to use in unit tests to implement the mock
// within the associated WritableKeyPath.
struct InjectedDependency {

    private static var current = InjectedDependency()

    static subscript<K>(key: K.Type) -> K.Value where K: InjectedKeyProtocol {
        get { key.currentValue }
        set { key.currentValue = newValue }
    }

    static subscript<T>(_ keyPath: WritableKeyPath<InjectedDependency, T>) -> T {
        get { current[keyPath: keyPath] }
        set { current[keyPath: keyPath] = newValue }
    }
}

// The Parent and Child keys to access the object in memory.
extension InjectedDependency {
  var parent: ParentProtocol {
    get { Self[ParentKey.self] }
    set { Self[ParentKey.self] = newValue }
  }

  var child: ChildProtocol {
    get { Self[ChildKey.self] }
    set { Self[ChildKey.self] = newValue }
  }
}

// The instantiation of the value linked to the key.
struct ParentKey: InjectedKeyProtocol {
    static var currentValue: ParentProtocol = Parent()
}

struct ChildKey: InjectedKeyProtocol {
    static var currentValue: ChildProtocol = Child()
}

很多变化,所以只是比较 - 通常我们需要考虑引用计数,即。谁保留参考...因此它仅适用于 reference-types.

测试 Xcode 13.3 / iOS 15.4

protocol ParentProtocol: AnyObject {}
class Parent: ParentProtocol {

  //var child: Child?
  @Injected(\.child) var child

  init() { print(" Allocating Parent in memory") }
  deinit { print ("♻️ Deallocating Parent from memory") }
}

protocol ChildProtocol: AnyObject {}
class Child: ChildProtocol {

  //weak var parent: Parent?
  @Injected(\.parent) var parent

  init() { print(" Allocating Child in memory") }
  deinit { print("♻️ Deallocating Child from memory") }
}

protocol InjectedKeyProtocol {
  associatedtype Value
  static var currentValue: Self.Value? { get set }
}

// The main dependency injection custom tool.
@propertyWrapper
struct Injected<T> {

    private let keyPath: WritableKeyPath<InjectedDependency, T?>

    var wrappedValue: T? {
        get { InjectedDependency[keyPath] }
        set { InjectedDependency[keyPath] = newValue }
    }

    init(_ keyPath: WritableKeyPath<InjectedDependency, T?>) {
        self.keyPath = keyPath
    }
}

// The custom tool to use in unit tests to implement the mock
// within the associated WritableKeyPath.
struct InjectedDependency {

    private static var current = InjectedDependency()

    static subscript<K>(key: K.Type) -> K.Value? where K: InjectedKeyProtocol {
        get { key.currentValue }
        set { key.currentValue = newValue }
    }

    static subscript<T>(_ keyPath: WritableKeyPath<InjectedDependency, T?>) -> T? {
        get { current[keyPath: keyPath] }
        set { current[keyPath: keyPath] = newValue }
    }
}

// The Parent and Child keys to access the object in memory.
extension InjectedDependency {
  var parent: ParentProtocol? {
    get { Self[ParentKey.self] }
    set { Self[ParentKey.self] = newValue }
  }

  var child: ChildProtocol? {
    get { Self[ChildKey.self] }
    set { Self[ChildKey.self] = newValue }
  }
}

// The instantiation of the value linked to the key.
struct ParentKey: InjectedKeyProtocol {
    static weak var currentValue: ParentProtocol?
}

struct ChildKey: InjectedKeyProtocol {
    static weak var currentValue: ChildProtocol?
}

测试代码的输出:

Complete test module in project is here