arrays - 在数组中保存和释放闭包

标签 arrays swift closures

我希望在不依赖响应式(Reactive)第三方库/框架的情况下创建一个可观察的属性。

我读了这篇文章,并提出了与他们的可观察属性答案类似的解决方案......

https://blog.scottlogic.com/2015/02/11/swift-kvo-alternatives.html

他们的

class Observable<T> {

  let didChange = Event<(T, T)>()
  private var value: T

  init(_ initialValue: T) {
    value = initialValue
  }

  func set(newValue: T) {
    let oldValue = value
    value = newValue
    didChange.raise(oldValue, newValue)
  }

  func get() -> T {
    return value
  }
}

我的

public class Observable<V> {

    public var value: V { didSet { for observer in observers { observer(value) } }}
    private var observers = [(V) -> Void]()

    public init(_ initital: V) {
        value = initital
    }

    public func observe(with closure: @escaping (V) -> Void) {
        observers.append(closure)
    }
}

唯一的区别是我想捕获一个闭包数组,而不是使用 EventaddHander...原因是我想提供传递通过值(value)而不是让我的代码的使用者一次又一次地创建一个函数而不依赖任何第三方代码。

我不确定一旦它们的所有者被释放,这些闭包如何自动从数组中删除。我猜他们不能,这就是使用 addHandler 的原因,我只是希望比我更有知识的人可以阐明这个问题。

感谢您的宝贵时间。

最佳答案

所以我想出了这个解决方案:

class Wrapper<V> {
    var observer: (V) -> Void
    public init(_ b: @escaping (V) -> Void) {
        observer = b
    }
}

class Observable<V> {
    public var value: V { didSet {
        let enumerator = observers.objectEnumerator()
        while let wrapper = enumerator?.nextObject() {
            (wrapper as! Wrapper<V>).observer(value)
        }
    }}
    private var observers = NSMapTable<AnyObject, Wrapper<V>>(keyOptions: [.weakMemory], valueOptions: [.strongMemory])

    public init(_ initital: V) {
        value = initital
    }

    public func observe(_ subscriber: AnyObject, with closure: @escaping (V) -> Void) {
        let wrapper = Wrapper(closure)
        observers.setObject(wrapper, forKey: subscriber)
    }
}

最终的 API 要求订阅者在调用时表明自己的身份:

Observable.observe(self /* <-- extra param */) { /* closure */ }

虽然我们不能弱引用闭包,但是使用NSMapTable,我们可以弱引用subscriber对象,然后将其用作跟踪观察者闭包的弱键。这允许取消分配订阅者,从而自动清理过时的观察者。

最后,这是演示代码。展开代码片段并复制粘贴到 Swift Playground 并实时查看。

import Foundation

func setTimeout(_ delay: TimeInterval, block:@escaping ()->Void) -> Timer {
    return Timer.scheduledTimer(timeInterval: delay, target: BlockOperation(block: block), selector: #selector(Operation.main), userInfo: nil, repeats: false)
}

class Wrapper<V> {
    var observer: (V) -> Void
    public init(_ b: @escaping (V) -> Void) {
        observer = b
    }
}

class Observable<V> {
    public var value: V { didSet {
        let enumerator = observers.objectEnumerator()
        while let wrapper = enumerator?.nextObject() {
            (wrapper as! Wrapper<V>).observer(value)
        }
    }}
    private var observers = NSMapTable<AnyObject, Wrapper<V>>(keyOptions: [.weakMemory], valueOptions: [.strongMemory])

    public init(_ initital: V) {
        value = initital
    }
    
    public func observe(_ subscriber: AnyObject, with closure: @escaping (V) -> Void) {
        let wrapper = Wrapper(closure)
        observers.setObject(wrapper, forKey: subscriber)
    }
}

class Consumer {
    private var id: String

    public init(_ id: String, _ observable: Observable<Int>) {
        self.id = id
        observable.observe(self) { val in
            print("[\(id)]", "ok, i see value changed to", val)
        }
    }
    
    deinit {
        print("[\(id)]", "I'm out")
    }
}

func demo() -> Any {
    let observable = Observable(1)
    var list = [AnyObject]()

    list.append(Consumer("Alice", observable))
    list.append(Consumer("Bob", observable))
    
    observable.value += 1

    // pop Bob, so he goes deinit
    list.popLast()
    
    // deferred
    setTimeout(1.0) {
        observable.value += 1
        observable.value += 1
    }

    return [observable, list]
}

// need to hold ref to see the effect
let refHolder = demo()


编辑:

正如 OP @Magoo 在下面评论的那样,Wrapper 对象未正确释放。即使 subscriber 对象已成功释放,并且相应的键已从 NSMapTable 中删除,Wrapper 仍作为 中保存的条目保持事件状态NSMapTable.

做了一些测试,发现确实是这样,出乎意料。一些进一步的研究揭示了一个不幸的事实:这是 NSMapTable 实现中的一个警告。

This post彻底解释背后的原因。直接引用Apple doc :

However, weak-to-strong NSMapTables are not currently recommended, as the strong values for weak keys which get zero’d out do not get cleared away (and released) until/unless the map table resizes itself.

嗯,所以基本上苹果只是认为可以将它们保留在内存中直到调整大小为止。从GC策略POV来看是合理的。

结论:如果 NSMapTables 实现保持不变,就不可能处理它。

但是对于大多数情况来说这应该不是问题。这个 Observer 实现按预期工作。只要Wrapper不做任何可疑的事情并且闭包不持有强引用,唯一的负面影响只是一些额外的内存占用。

我确实有一个解决方案,您可以使用 weak ->weak 映射,因此 Wrapper 作为弱值也得到释放。但这需要 .observe() 返回 Wrapper,然后 Consumer 获取对其的引用。我不喜欢这个想法,API 对于最终用户来说不符合人体工程学。我宁愿忍受一些内存开销,也有利于更好的 API。

编辑2:

我不喜欢上述修复,因为生成的 API 不友好。我别无选择,但@Magoo 成功解决了!使用 objc_setAssociatedObject API,这是我以前从未听说过的。请务必结账 his answer详细一点,太棒了。

关于arrays - 在数组中保存和释放闭包,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55649572/

相关文章:

javascript - 将js数组转换为php数组并检查其内容

javascript - JavaScript 类中的 setTimeout() 使用 "this"

closures - 在 Elixir 中创建闭包

c - 将动态分配的数组传递给 C 中的函数是按值传递还是按引用传递的实例?

javascript - 将字符串中的单词提取到数组中

ios - detailTextLabel 未显示,但已设置 100%

ios - swift 将 CLLocation 转换为 CLLocationCoordinate2D

使用闭包在循环中创建的javascript计时器或间隔

php - Guzzle 使用嵌套数组发布多部分请求

ios - 更新到 Xcode 7.1(从 6.4)导致 UIIMageView 动画损坏