Swift - Observe

介绍

Swift 4.0 增加了新的 KVO 方式, 代码大大减少,并且不再需要手动 removeObserver

先来一个例子看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyObjectToObserve: NSObject {
@objc dynamic var myDate = NSDate()
func updateDate() {
myDate = NSDate()
}
}

class MyObserver: NSObject {
@objc var objectToObserve: MyObjectToObserve
var observation: NSKeyValueObservation?

init(object: MyObjectToObserve) {
objectToObserve = object
super.init()

observation = observe(\.objectToObserve.myDate) { object, change in
print("Observed a change to \(object.objectToObserve).myDate, updated to: \(object.objectToObserve.myDate)")
}
}
}

let observed = MyObjectToObserve()
let observer = MyObserver(object: observed)

observed.updateDate()

以上就是官方实例,可以看到,代码比较以前减少了很多,使用也更加方便

但是,由于 KVO 是 OC 下的特性,所以只有继承 NSObject 的子类才可以使用 KVO,并且被观察的属性需要用 dynamic 修饰

还有需要注意的是,虽然不需要手动 removeObserver,但也导致观察的闭包没有强引用,当前作用域结束时,就会被释放,所以需要我们手动强引用,保证闭包不被收回

实现

Swift 代码是开源的,所以我们可以看一下 Observe 是如何实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public func observe<Value>(
_ keyPath: KeyPath<Self, Value>,
options: NSKeyValueObservingOptions = [],
changeHandler: @escaping (Self, NSKeyValueObservedChange<Value>) -> Void)
-> NSKeyValueObservation {
let result = NSKeyValueObservation(object: self as! NSObject, keyPath: keyPath) { (obj, change) in
let notification = NSKeyValueObservedChange(kind: change.kind,
newValue: change.newValue as? Value,
oldValue: change.oldValue as? Value,
indexes: change.indexes,
isPrior: change.isPrior)
changeHandler(obj as! Self, notification)
}
result.start(options)
return result
}

这个方法定义了三个参数,分别是 keyPath options changeHandler,最后返回了一个 NSKeyValueObservation

  • keyPath 是 Swift 4 新增的 KeyPath 类型,与 Swift 3 中的 #KeyPath 不同,它更加安全,性能更好
  • options 可选参数,默认传入空数组
  • changeHandler 当观察属性发生改变时调用的回调

其中最关键的就是 NSKeyValueObservation,通过它实现了自释放

NSKeyValueObservation

下面是这个类的内容,并不复杂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class NSKeyValueObservation : NSObject {

weak var object : NSObject?
let callback : (NSObject, NSKeyValueObservedChange<Any>) -> Void
let path : String

//workaround for <rdar://problem/31640524> Erroneous (?) error when using bridging in the Foundation overlay
static var swizzler : NSKeyValueObservation? = {
let bridgeClass: AnyClass = NSKeyValueObservation.self
let observeSel = #selector(NSObject.observeValue(forKeyPath:of:change:context:))
let swapSel = #selector(NSKeyValueObservation._swizzle_me_observeValue(forKeyPath:of:change:context:))
let rootObserveImpl = class_getInstanceMethod(bridgeClass, observeSel)
let swapObserveImpl = class_getInstanceMethod(bridgeClass, swapSel)
method_exchangeImplementations(rootObserveImpl, swapObserveImpl)
return nil
}()

fileprivate init(object: NSObject, keyPath: AnyKeyPath, callback: @escaping (NSObject, NSKeyValueObservedChange<Any>) -> Void) {
path = _bridgeKeyPathToString(keyPath)
let _ = NSKeyValueObservation.swizzler
self.object = object
self.callback = callback
}

fileprivate func start(_ options: NSKeyValueObservingOptions) {
object?.addObserver(self, forKeyPath: path, options: options, context: nil)
}

///invalidate() will be called automatically when an NSKeyValueObservation is deinited
public func invalidate() {
object?.removeObserver(self, forKeyPath: path, context: nil)
object = nil
}

@objc func _swizzle_me_observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSString : Any]?, context: UnsafeMutableRawPointer?) {
guard let ourObject = self.object, object as? NSObject == ourObject, let change = change else { return }
let rawKind:UInt = change[NSKeyValueChangeKey.kindKey.rawValue as NSString] as! UInt
let kind = NSKeyValueChange(rawValue: rawKind)!
let notification = NSKeyValueObservedChange(kind: kind,
newValue: change[NSKeyValueChangeKey.newKey.rawValue as NSString],
oldValue: change[NSKeyValueChangeKey.oldKey.rawValue as NSString],
indexes: change[NSKeyValueChangeKey.indexesKey.rawValue as NSString] as! IndexSet?,
isPrior: change[NSKeyValueChangeKey.notificationIsPriorKey.rawValue as NSString] as? Bool ?? false)
callback(ourObject, notification)
}

deinit {
object?.removeObserver(self, forKeyPath: path, context: nil)
}
}

这个类很简单,可以很清除的了解到它做了什么,可以看出,它其实是把原本的 KVO 做了一层封装

它负责了 KVO 的生命周期,当手动停止或者生命周期结束时,会自动 removeObserver,并且把监听事件作为一个闭包传进来,避免了之前复杂的写法

感觉对这个类,与 OC 中 KVOController 库的实现十分类似,可以通过学习 KVOController 来了解这一思想

相关链接

Smart KeyPaths
Swift Github - Observe
Key-Value Observing
KVOController