以数组形式保存和释放闭包

时间:2019-04-12 10:26:45

标签: arrays swift closures

我希望在不依赖于被动的第三方库/框架的情况下提供可观察的属性。

我阅读了此书,并为他们的“可观察属性”答案提出了类似的解决方案...

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 ...原因是我想提供传递值的语法而不是使用我的代码的使用者每次都会创建函数,以不依赖任何第三方代码。

我不确定一旦释放了这些闭包,如何自动将其从数组中删除。我猜他们不能这样做,这就是为什么使用addHandler的原因,我只是希望那里的人知识渊博,比我可以阐明这个问题。

感谢您的时间。

3 个答案:

答案 0 :(得分:1)

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

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对象,然后将其用作跟踪观察者闭包的弱键。这允许释放subscriber,从而自动清除过时的观察者。

最后,这是演示的代码。展开摘要,然后将其粘贴粘贴到快速的操场上,并实时观看。

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

  

但是,当前不建议使用弱到强的NSMapTables,因为直到/除非映射表自行调整大小,否则弱零的强值不会被清零(并释放)。

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

结论:如果NSMapTables实现保持不变,将不会有任何机会。

但是,在大多数情况下,这应该不是问题。此Observer隐含功能可以正常工作。只要Wrapper不做任何可疑的事情,并且闭包不具有强大的引用,那么负面影响就是一些额外的内存占用。

不过我确实有个解决办法,您可以使用weak -> weak映射,因此Wrapper作为一个弱值也可以取消分配。但这需要.observe()返回Wrapper,然后Consumer保留对它的引用。我不喜欢这个想法,对于最终用户而言,API并不符合人体工程学。我宁愿留一些内存开销来支持更好的API。

编辑2:

我不喜欢上述修复程序,导致生成的API不友好。我没有办法,但@Magoo设法钉住了它!使用我以前从未听说过的objc_setAssociatedObject API。请务必查看his answer以获得详细信息,这太棒了。

答案 1 :(得分:1)

好的,@ hackape用objc_setAssociatedObject回答

public class Observable<V> {

    private class ClosureWrapper<V> {
        var closure: (V) -> Void
        public init(_ closure: @escaping (V) -> Void) {
            self.closure = closure
        }
    }

    private var observers = NSMapTable<AnyObject, ClosureWrapper<V>>(keyOptions: [.weakMemory], valueOptions: [.weakMemory])
    public var value: V { didSet { notify() } }

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

    public func addObserver(_ object: AnyObject, skipFirst: Bool = true, closure: @escaping (V) -> Void) {
        let wrapper = ClosureWrapper(closure)
        let reference = "observer\(UUID().uuidString)".replacingOccurrences(of: "-", with: "")
        observers.setObject(wrapper, forKey: object)

        // Giving the closure back to the object that is observing
        // allows ClosureWrapper to die at the same time as observing object
        objc_setAssociatedObject(object, reference, wrapper, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        if !skipFirst { closure(value) }
    }

    private func notify() {
        let enumerator = observers.objectEnumerator()
        while let wrapper = enumerator?.nextObject() { (wrapper as? ClosureWrapper<V>)?.closure(value) }
    }
}

这个人还使用一种非常相似的方法https://codereview.stackexchange.com/questions/85709/generic-nsmaptable-replacement-written-in-swift在Swift中重新制作了NSMapTable

答案 2 :(得分:0)

最简单且可能最安全的解决方案是使用您拥有的确切实现,但是在执行任何操作/副作用之前,请确保所有调用者都使用[weak self]并确保self仍然存在。

这样,当执行闭包数组时,所有创建者已经取消分配的对象都将在调用时立即返回。

// called from outside class
observer.observe { [weak self] in 
    guard strongSelf = self else { return }

    // do work using `strongSelf`
}

如果观察者将被不断释放的许多实例所使用,我建议添加一个remove观察者函数。为此,您可能希望在observe调用中返回一个字符串,然后将其用于删除闭包。与此类似:

public typealias ObserverIdentifier = String
public class Observable<V> {
    public var value: V { didSet { for observer in observers.values { observer(value) } }}
    private var observers = [ObserverIdentifier  : (V) -> Void]()

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

    @discardableResult public func observe(with closure: @escaping (V) -> Void) -> ObserverIdentifier {
        let identifier = UUID().uuidString
        observers[identifier] = closure
        return identifier
    }

    public func remove(identifier: ObserverIdentifier) {
        observers.removeValue(forKey: identifier)
    }
}

因为您使用的是[weak self],所以在取消分配时删除观察者只是一件好事,可以避免一些其他的无操作,但是如果不删除,仍然是完全安全的。