Swift之KVO属性关系依赖

2019-02-02

导语

很显然,在Swift中,观察一个值类型或者引用类型的属性的内容变化,我们可以利用属性观察器willSetdidSet,但对于KVO/KVC这样强大的机制却不能直接使用,毕竟KVO/KVC仅仅是OC的运行时特性,Swift不具有的,要想要使用这个KVO/KVC,必须要继承NSObject。这样以来,Swift中的结构体与枚举类型这样的值类型是不能用KVO/KVC特性。因此仅仅针对能继承NSObjectclass类型。这是我们使用KVO/KVC的前提,虽然有局限性,但是有时候真的能够带来方便。比如说,在某些情况下,一个属性的值取决于另一个对象中的一个或者多个属性值时候,利用KVO的特效就相当的方便了。

KVO(Key-Value Observing)

KVO:健值观察,是观察者模式的实现,提供一种观察对象属性内容变化的机制。那如何实现的呢?苹果官方文档针对KVO实现原理(Key-Value Observing Implementation Details)是这样阐述的:

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

可以看出,KVO是通过一种叫isa-swizzling技术实现的。什么是isa-swizzling呢?顾名思义,isa指针指向维护调度表的对象类,调度表基本包含了指向该类的实现方法的指针,以及其他数据。当观察者注册对象的属性时,观察对象的isa指针被修改,指向中间类而不是真正的类。通俗的讲,每个类对象都有一个isa指针,该指针指向当前类,当这个类对象的属性被观察时,系统会动态生成一个中间类(派生类),然后把这个isa指针指向这个中间类,在这个中间类重写被观察属性的setter方法,在这个中间类重写的setter方法实现真正的通知机制。这个通知机制又使用了willChangeValueForKey(Swift中willChangeValue(forKey key: String))和didChangeValueForKey(Swift中didChangeValue(forKey key: String))这两个方法。

使用KVO的基本步骤是:

  1. 调用addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?)
  2. 重写函数 observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
  3. 在不需要观察属性的时候移除removeObserver(_ observer: NSObject, forKeyPath keyPath: String)

值得注意的是:

  1. 如果观察者被多次移除,程序崩溃
  2. 多次添加观察者,程序崩溃
  3. 观察者被deinit了,但没有移除监听,程序崩溃

场景

一个人的姓名取决于姓氏(lastName)与名(firstName),我们可以把全名的方法定义成这样

1
2
3
var fullName: String! {
return lastName + firstName
}

fullName 的变化 依赖于lastNamefirstName。当其中一个变化都应该去通知观察属性的对象。如何解决呢?一种解决方案重写class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String>,另一种是通过实现遵循命名约定的类方法来实现相同的结果keyPathsForValuesAffecting<Key>,其中<Key>是依赖于值的属性(首字母大写)的名称。

基本实现

这是一种To-One Relationships(一对一关系)

创建一个Person类,继承了NSObject,我们使用的是runtime,需要dynamic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 class Person: NSObject {
@objc dynamic var fullName: String! {
return lastName + firstName
}
@objc dynamic var firstName: String! = "能"
@objc dynamic var lastName: String! = "刘"
// 第一种方法利用 重写 函数 keyPathsForValuesAffectingValue 实现
// override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
// if key.isEmpty {
// return Set()
// }
// var keyPaths: Set = super.keyPathsForValuesAffectingValue(forKey: key)
// if key == "fullName" {
// let affectedKey: Array = ["firstName","lastName"]
// keyPaths = keyPaths.union(affectedKey)
// }
// return keyPaths
// }
//第二种方法使用 keyPathsForValuesAffecting<Key> 实现,其中 Key是 需要观察的属性
@objc class func keyPathsForValuesAffectingFullName() -> Set<String> {
return Set(arrayLiteral: "firstName","lastName")
}
}

创建一个 观察类,使用两个定时器,模拟改变这两个属性的值

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
private var ObserverContent = 0
class Observer: NSObject {
var ob: Person!
let firstNames:Array = ["三","四","五","六"]
let lastNames:Array = ["张","李","王","赵"]
var firtNameCount: Int = 0
var lastNameCount: Int = 0
override init() {
super.init()
ob = Person()
//添加观察者
ob.addObserver(self, forKeyPath: "fullName", options: .new, context: &ObserverContent)
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { (_) in
self.ob.lastName = self.lastNames[self.lastNameCount]
self.lastNameCount += 1
if self.lastNameCount >= self.lastNames.count {
self.lastNameCount = 0
}
}
Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { (_) in
self.ob.firstName = self.firstNames[self.firtNameCount]
self.firtNameCount += 1
if self.firtNameCount >= self.firstNames.count {
self.firtNameCount = 0
}
}
}
// 重写
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let ch = change, context == &ObserverContent {
let a = ch[NSKeyValueChangeKey.newKey]
print(a! as Any)
}else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}

deinit {
//移除
self.ob.removeObserver(self, forKeyPath: "fullName")
}
}

创建 对象

1
let observer = Observer()

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
刘三
张三
张四
李四
李五
李六
王六
王三
赵三
赵四
赵五
张五
张六
李六
李三

还有另一种写法,我就不做解释了。直接上代码吧,这种方式在Swift4.x不需要调用romove观察者。

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
class Foo: NSObject {
@objc dynamic var fullName: String {
return firstName + lastName
}
@objc dynamic var firstName: String
@objc dynamic var lastName: String
@objc var observertion: NSKeyValueObservation?
override init() {
firstName = ""
lastName = ""
// 需要 把 属性初始化才能调用 super.init()
super.init()
Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { (_) in
self.firstName = "四"
}
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { (_) in
self.lastName = "李"
}
}
@objc class func keyPathsForValuesAffectingFullName() -> Set<String> {
return Set(arrayLiteral: "firstName","lastName")
}
func begin() -> Void {
self.observertion = self.observe(\.fullName) { (foo, change) in
print(" \(foo.fullName)")
}
}
func stop() -> Void {
Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { (_) in
self.observertion?.invalidate()
}
}
}
//let foo = Foo()
//foo.begin()
//foo.stop()

相关资料推荐

Key Value Observation in iOS 11

Swift4.2 Hits the Road

KVO和KVC实现