我希望在不依赖响应式(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)
}
}
唯一的区别是我想捕获一个闭包数组,而不是使用 Event
和 addHander
...原因是我想提供传递通过值(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/