代码之家  ›  专栏  ›  技术社区  ›  Magoo

在数组中保持和释放闭包

  •  0
  • Magoo  · 技术社区  · 6 年前

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

    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)
        }
    }
    

    唯一的区别是我想捕获闭包数组,而不是使用 Event addHander ... 原因是我想提供传递值的语法,而不是让代码的使用者每次都做一个函数,不依赖任何第三方代码。

    addHandler 我只是希望有人比我更了解这个问题。

    谢谢你抽出时间。

    0 回复  |  直到 6 年前
        1
  •  1
  •   hackape    6 年前

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

    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 对象,然后将其用作跟踪观察者闭包的弱关键点。这允许取消分配 用户 因此,自动清理过时的观察者。

    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 对象未正确释放。即使 用户 对象已成功解除分配,并且相应的密钥已从 NSMapTable 这个 NSMapTable

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

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

    嗯,所以苹果公司基本上认为,在重新调整大小之前,把它们保存在内存中是可以的。从GC策略POV来看是合理的。

    结论:如果 NSMapTables 执行情况保持不变。

    Observer impl按预期工作。只要 不要做任何可疑的事情,闭包也不支持强引用,唯一的负面影响只是一些额外的内存占用。

    不过我有一个补丁,你可以用 weak -> weak 包装纸 作为一个弱值,也要获得dealloc。但这需要 .observe() 返回 包装纸 Consumer 找到它的参考号。我对这个想法不感兴趣,API对最终用户来说不符合人体工程学。为了更好的API,我宁愿忍受一些内存开销。

    编辑2:

    我不喜欢前面提到的修复,因为生成的API不友好。我看不出有什么办法,但@Magoo设法解决了!使用 objc_setAssociatedObject API,我以前从未听说过。一定要结帐 his answer 就细节而言,这太棒了。

        2
  •  1
  •   Magoo    6 年前

    好的,那么@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) }
        }
    }
    

    NSMapTable 在Swift中使用一种非常类似的方法 https://codereview.stackexchange.com/questions/85709/generic-nsmaptable-replacement-written-in-swift

        3
  •  0
  •   Casey    6 年前

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

    这样,当执行闭包数组时,任何创建者已经dealloc的闭包都将在调用时立即返回。

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

    如果观察器将被许多不断释放的实例使用,我建议添加一个removeobserver函数。要执行此操作,您可能需要在 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)
        }
    }
    

    [虚弱的自我]