iOS设计模式 - KVO原理和使用

KVO/KVC是观察者模式在Objective-C中的实现,以非正式协议(Category)的形式被定义在NSObject中。从协议的角度看,是定义了一套让开发者遵守的规范和使用的方法。

在Cocoa的MVC框架中,KVO架起ViewController和Model沟通的桥梁,例如:

代码中,在模型类A创建属性数据,在控制器中创建观察者,一旦属性数据发生改变就收到观察者收到通知,通过KVO再在控制器使用回调方法处理实现视图B的更新。

实现原理

当观察某对象A时,KVO机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keyPath的setter方法,setter方法随后负责通知观察对象属性的改变状况。Apple使用了:

isa 混写(isa-swizzling)isa:is a kind of ; swizzling:混合,搅合;

来实现 KVO,当观察对象A时,KVO机制动态创建一个新的名为:

NSKVONotifying_A 的继承自对象A的子类,

且KVO为NSKVONotifying_A重写了观察属性的setter方法,setter方法会负责在调用原setter方法之前和之后,通知所有观察对象属性值的更改情况。

NSKVONotifying_A类剖析:

在观察到对象成员有变化时,被观察对象的isa指针从指向原来的A类,被KVO机制修改为指向:

系统新创建的子类 NSKVONotifying_A类,

通过NSKVONotifying_A的setter方法来实现当前类属性值改变的监听;

所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统隐瞒了对KVO的底层实现过程,让我们误以为还是原来的类。

但是此时如果我们创建一个新的名为NSKVONotifying_A的类,就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候:

动态的创建了名为NSKVONotifying_A的中间类,并指向这个中间类。

因此由于类名的冲突,就会引起程序的崩溃。

isa指针的作用:

每个对象都有isa指针,指向该对象的类,它告诉Runtime系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么:

这个被观察的对象就神奇地变成新子类的对象或者说实例了。

因此在该对象上对sette 的调用就会调用已重写的 setter,从而激活键值通知机制。

子类setter方法剖析:

KVO的键值观察通知依赖于NSObject 的两个方法:

willChangeValueForKey:

didChangevlueForKey:

  • 被观察属性发生改变之前willChangeValueForKey:被调用,通知系统该 keyPath的属性值即将变更;

    • 改变发生后didChangeValueForKey:被调用,通知系统该keyPath的属性值已经变更;

      • 在上面的方法执行之后:

        observeValueForKey:ofObject:change:context:

        也会被调用。且重写观察属性的setter方法是在运行时而不是编译时实现的。

KVO为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

1
2
3
4
-(void)setName:(NSString *)newName{
[self willChangeValueForKey:@"name"]; //KVO在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO在调用存取方法之后总调用}

KVO的主要方法

KVO的主要方法有以下几个:

addObserver调用方法

1
object.addObserver(observer, forKeyPath: "name", options: 0, context: &context)

addObserver方法用于添加观察者,监听属性的变化,它的几个参数作用如下:

  • object:是被观察者的实例;
  • observer:是观察者的实例,这个对象要实现下面的observeValue方法;
  • forKeyPath:是观察者实例要监听的键,如UIView的frame、center等;
  • options:有4个值,5种情况,具体请看下文;
  • context:它是一个C指针,会指向希望监听的属性。如:&object->name,同时它也可以带入一些参数,可以是任意类型,需要自己强转。

observeValue处理方法

1
func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)

observeValue方法用于监听属性的变化,使重写此方法的类具有监听的能力,它的几个参数作用如下:

  • keyPath:对应forKeyPath
  • object:被观察的对象
  • change:对应options
  • context:对应context

removeObserver移除属性观察者

1
2
object.removeObserver(observer, forKeyPath: "name", context: &context)
object.removeObserver(observer, forKeyPath: "name")

需要注意的是,如果注册observer时传入了context,之后一定要在合适的机会解除,否则会引发资源泄露。

options参数

options参数是OptionSet类型,它是一个结构体,有以下四个类型属性:

1
2
3
4
5
6
7
public struct NSKeyValueObservingOptions : OptionSet {
public init(rawValue: UInt)
public static var new: NSKeyValueObservingOptions { get }
public static var old: NSKeyValueObservingOptions { get }
public static var initial: NSKeyValueObservingOptions { get }
public static var prior: NSKeyValueObservingOptions { get }
}

这四个值的含义如下:

new:

表明变化的字典应该提供新的属性值,接收方法中使用change参数传入变化后的新值,键为NSKeyValueChange.newKey;

old:

表明变化的字典应该包含旧的属性值,接收方法中使用change参数传入变化前的旧值;键为NSKeyValueChange.oldKey;

initial:

把初始化的值提供给处理方法,一旦注册,马上就会调用一次,如果配置了new,change参数内容会包含新值;它是如何使用的呢?比如我们在进行UI相关的KVO操作时候,通常会遇到这样的需求:

在添加通知后,立即发送一个改变通知告诉UI去更新界面。

这会让我们的界面有一个初始状态。我们可以指定Initial选项,这样在我们添加完KVO监听后,属性改变的通知就会立即被执行一次:

prior:

接收方法会在变化前后分别调用一次,共两次,change参数内容根据new和old的配置确定。这个选项可以让我们在被监听的属性改变的时候得到两个通知,一个是在属性值改变之前,一个是属性值改变之后。

然后就可以在 observeValueForKeyPath中的change字典中以NSKeyValueChangeNotificationIsPriorKey键来表示当前通知是不是在属性被修改之前发送的:

1
2
3
4
5
6
7
8
9
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let _ = change?[NSKeyValueChangeNotificationIsPriorKey] {
print("old")
}
else {
print("new")
}
}

当然,如果你只是想得到修改前和修改后的值,那么也可以用change字段中的NSKeyValueChangeOldKeyNSKeyValueChangeNewKey来得到相应的值:

1
2
3
4
5
6
7
if let newFirstName = change?[NSKeyValueChangeNewKey] as? String {
}
if let oldFirstName = change?[NSKeyValueChangeOldKey] as? String {
}

change参数

change参数是NSKeyValueChangeKey类型,它是一个结构体:

1
2
3
4
5
6
7
8
9
10
public struct NSKeyValueChangeKey : RawRepresentable, Equatable, Hashable, Comparable {
public init(rawValue: String)
}
extension NSKeyValueChangeKey {
public static let kindKey: NSKeyValueChangeKey
public static let newKey: NSKeyValueChangeKey
public static let oldKey: NSKeyValueChangeKey
public static let indexesKey: NSKeyValueChangeKey
public static let notificationIsPriorKey: NSKeyValueChangeKey
}

它的几个参数作用如下:

  • kindKey:用于传递被监听属性的变化类型

    1
    2
    3
    4
    5
    6
    public enum NSKeyValueChange : UInt {
    case setting
    case insertion
    case removal
    case replacement
    }
    • setting:属性的值被重新设置;
    • insertion & removal & replacement:表示更改的是集合属性,分别代表插入、删除、替换操作。
  • newKey:如果KindKey是setting,那么newKey对应的为新设置的值;

  • oldKey:如果KindKey是setting,那么oldkey对应的为上一个值;

  • indexesKey:如果kindkey是.insertion, .removal, .replacement,那么这个值对应的为NSIndexSet (包含相应操作的indexs);

  • priorKey:表明收到的是值改变之前的通知,对应着addobserver中option的.prior;

了解KVO的主要方法和参数后,我们就可以开始使用KVO进行编程了,下面我们开始进行:

KVO的使用

首先定义一个 Persson 类,当做被观察的对象:

1
2
3
4
5
6
class Person: NSObject {
var address: String?
var weight: Float?
var name: String?
var age: Int?
}

再定义一个 PersonObserving 类并重写observeValue方法,用于观察Person

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PersonObserving: NSObject {
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
let newValue = change?[NSKeyValueChangeKey.newKey]
let oldValue = change?[NSKeyValueChangeKey.oldKey]
let person = object as! Person
let con = context?.load(as: String.self)
print("观察到\(keyPath)的变化,旧的值是\(oldValue),新的值是\(newValue)")
print("观察的类是\(type(of: person))")
print("接收的上下文是\(con)")
}
}

观察者和被观察者准备就绪,即可进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**执行*/
let person = Person()
let personObserving = PersonObserving()
var context = "this is a context"
person.addObserver(personObserving,
forKeyPath: "name",
options: NSKeyValueObservingOptions.new,
context: &context)
person.setValue("Jerk ass", forKey: "name")
print(person.value(forKey: "name"))
person.removeObserver(personObserving,
forKeyPath: "name",
context: &context)
/**输出*/
观察到Optional("name")的变化,旧的值是nil,新的值是Optional(Jerk ass)
观察的类是Person
接收的上下文是Optional("this is a context")
Optional(Jerk ass)

KVO和keyPath

如果Person类里面还有个再添加一个Job的属性:

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
class Person: NSObject {
var address: String?
var weight: Float?
var name: String?
var age: Int?
var job = Job()
}
class Job: NSObject {
var companyName: String?
var salary: NSNumber?
}
/**执行*/
person.addObserver(personObserving, forKeyPath: "job.salary",
options: NSKeyValueObservingOptions.new,
context: &context)
person.setValue("20000.00", forKeyPath: "job.salary")
print(person.value(forKeyPath: "job.salary"))
person.removeObserver(personObserving,
forKeyPath: "job.salary",
context: &context)
/**输出*/
观察到Optional("job.salary")的变化,旧的值是nil,新的值是Optional(20000.00)
观察的类是Person
接收的上下文是Optional("this is a context")
Optional(20000.00)

要观察和设置Person的薪水,只要这么写就可以了。

KVO属性依赖

假如有个Person类,类里有三个属性,fullNamefirstNamelastName。按照之前的知识,如果需要观察名字的变化,就要分别添加 fullNamefirstNamelastName 三次观察,非常麻烦。

如果能够只观察 fullName,并建立fullNamefirstNamelastName 的某种依赖关系,当发生变化时,也受到通知,那需要怎么做呢?KVC提供这种键之间的依赖方法,格式如下:

1
class func keyPathsForValuesAffecting<Key>() -> NSSet

这方法使得Key之间能够建立依赖关系,为了便于说明,直接用属性依赖这个词代替Key之间的依赖。含义不同,结果一致,下面就使用这种方法解决 Key 之间的依赖关系,首先将Person类设置为被观察者:

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 Person: NSObject {
var firstName: String?
var lastName: String?
var fullName: String? {
get {
return firstName! + lastName!
}
}
override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
print(#function)
if key == "fullName" {
return Set<String>(arrayLiteral: "firstName","lastName")
} else {
return super.keyPathsForValuesAffectingValue(forKey: key)
}
}
}

ViewController 类设置为观察者:

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
class ViewController: UIViewController {
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
let newValue = change?[NSKeyValueChangeKey.newKey]
let oldValue = change?[NSKeyValueChangeKey.oldKey]
print("观察到\(keyPath)的变化,旧的值是\(oldValue),新的值是\(newValue)")
}
}
/**执行*/
let person = Person()
person.lastName = "hoohoo"
var context = "this is a context"
person.addObserver(self,
forKeyPath: "fullName",
options: NSKeyValueObservingOptions.new,
context: &context)
person.setValue("Bad ass ", forKeyPath: "firstName")
person.removeObserver(self, forKeyPath: "fullName", context: &context)
print(person.value(forKeyPath: "fullName"))
/**输出*/
keyPathsForValuesAffectingValue(forKey:)
keyPathsForValuesAffectingValue(forKey:)
keyPathsForValuesAffectingValue(forKey:)
keyPathsForValuesAffectingValue(forKey:)
keyPathsForValuesAffectingValue(forKey:)
观察到Optional("fullName")的变化,旧的值是nil,新的值是Optional(Bad ass hoohoo)
Optional(Bad ass hoohoo)

通过输出结果,我们可以判断出:发现虽然观察的是fullName,但是当修改firstName的时候,观察者也会收到fullName变化的通知,达到了我们的期望。

重写keyPathsForValuesAffectingValueForKey方法的时候有一点需要注意,这个方法在Objective-C中的签名是这样:

(NSSet )keyPathsForValuesAffectingValueForKey:(NSString )key;

如果照着这个逻辑,我们很可能在 Swift 中将这个方法的声明写成这样:

override class func keyPathsForValuesAffectingValueForKey(key: NSString) -> NSSet;

如果这样写,编译器就会报错。因为Swift 中将Objective-C的一些基础类型都已经转变成了Swift的原生类型,所以我们这个方法签名要写成这样才可以通过编译:

override class func keyPathsForValuesAffectingValueForKey(key: String) -> Set。

KVO手动通知

KVO 在默认情况下,只要为某个属性添加了监听对象,在这个属性值改变的时候,就会自动的通知监听者。也有一些情况下,可能我们想手动的处理这些通知的发送, KVO 也是允许我们这样做的。我们可以通过重写

class func automaticallyNotifiesObservers(forKey key:String) -> Bool

这个方法告诉KVO,哪些属性是我们想手动处理的,比如我们的Person类中,想对firstName进行处理,就在方法中对firstName这个key返回false

dynamic关键字

注意下面我们使用了dynamic标识。这个代表它支持Objective-C Runtime的动态分发机制,我们这里可以理解为 KVO需要使用Objective-C的Runtime机制来实现属性更改的监听。

Swift中的属性处于性能等方面的考虑默认是关闭动态分发的,所以我们这里面要显式的将属性用dynamic关键字标识出来。

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
class Person: NSObject {
dynamic var firstName: String? {
willSet {
print("\(#function) 将要修改,现在的值是 \(newValue)")
self.willChangeValue(forKey: "firstName")
}
didSet {
self.didChangeValue(forKey: "firstName")
print("\(#function) 已修改,原来的值是 \(oldValue)")
}
}
override func willChangeValue(forKey key: String) {
print("\(key) 将要修改")
}
override func didChangeValue(forKey key: String) {
print("\(key) 已修改")
}
override class func automaticallyNotifiesObservers(forKey key:String) -> Bool {
if key == "firstName" {
return false
} else {
return true
}
}
}
/**执行*/
let person = Person()
var context = "this is a context"
person.addObserver(self,
forKeyPath: "firstName",
options: NSKeyValueObservingOptions.new,
context: &context)
person.setValue("Bad ass ", forKeyPath: "firstName")
person.removeObserver(self, forKeyPath: "firstName", context: &context)
print(person.value(forKeyPath: "firstName"))
/**输出*/
keyPathsForValuesAffectingValue(forKey:)
firstName 将要修改,现在的值是 Optional("Bad ass ")
firstName 将要修改
firstName 已修改
firstName 已修改,原来的值是 nil
Optional(Bad ass )

这样,我们在修改了fistName属性值后,并不会触发KVO的默认通知行为,是我们自己来控制通知的发送,当然,这需要我们修改firstName属性的实现来进行手工的通知发送,手动通知能让我们对KVO通知进行更细节的控制。但并不常用,大多数情况下使用KVO的自动通知机制就足够了。

KVO和线程

一个需要注意的地方是,KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用-didChange...会触发 KVO 通知。

所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把KVO和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 KVO。

KVO是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO会保证下列两种情况的发生:

  • 首先,如果我们调用一个支持KVO的setter方法,如下所示:

    1
    self.name = "conqueror"

    KVO能保证所有name的观察者在setter方法返回前被通知到。

  • 其次,如果某个键被观察的时候附上了NSKeyValueObservingOptionPrior这个选项,直到

    1
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)

    被调用之前, namegetter方法都会返回同样的值。

理解KVO的实现

基本的流程就是编译器自动为被观察对象创造一个派生类,并将被观察对象的isa指向这个派生类。如果用户注册了对某此目标对象的某一个属性的观察,那么此派生类会重写这个方法,并在其中添加进行通知的代码。

Objective-C在发送消息的时候,会通过isa指针找到当前对象所属的类对象。而类对象中保存着当前对象的实例方法,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法。

由于编译器对派生类的方法进行了override,并添加了通知代码,因此会向注册的对象发送通知。注意派生类只重写注册了观察者的属性方法。

总结

KVO是Cocoa提供的一个很强大的特性,但同时它也有很多坑需要我们注意,比如添加完监听后,要在不需要的时候删除掉监听,否则就会造成意外崩溃。对于有依赖关系的属性需要通过:

1
override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set

这个方法将依赖关系声明到实体类中。还有各个监听选项的作用不同组合也有不同的作用;

另外需要注意的是观察者观察的是属性,只有遵循KVO变更属性值的方式才会执行KVO的回调方法,例如是否执行了setter方法、或者是否使用了KVC赋值。

如果赋值没有通过setter方法或者KVC,而是直接修改属性对应的成员变量,例如:仅调用person.name = "newName",这时是不会触发KVO机制,所以使用KVO机制的前提是遵循 KVO 的属性设置方式来变更属性值。

对比其他的回调方式,KVO机制的运用的实现,更多的由系统支持,相比Notification、Delegate等更简洁些,并且能够提供观察属性的最新值以及原始值;但是相应的在创建子类、重写方法等等方面的内存消耗是很巨大的。

由于KVO对被观察者的继承和重写是在运行时而不是编译时实现的,如果给定的实例没有观察者,那么KVO不会有任何开销,因为此时根本就没有KVO代码存在。但是即使没有观察者,Delegate和NSNotification还是得工作,这也是KVO此处零开销观察的优势。

所以对于两个类之间的通信,我们可以根据实际开发的环境采用不同的方法,使得开发的项目更加简洁实用。

KVC与KVO的区别

KVC(键值编码),即Key-Value Coding,一个非正式的Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用Setter、Getter方法等显式的存取方式去访问。
KVO(键值监听),即Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了Setter方法、或者使用了KVC赋值。

KVO与Notification的区别

Notification比KVO多了发送通知的一步,两者都是一对多,但是对象之间直接的交互,Notification需要NotificationCenter来做为中间交互。而KVO如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了。

Notification的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。

KVO与Delegate的区别

和Delegate一样,KVO和NSNotification的作用都是类与类之间的通信。

但是KVO和NSNotification与Delegate不同的是,KVO和NSNotification都是负责发送接收通知,剩下的事情由系统处理,所以不用返回值;

而Delegate则需要通信的对象通过变量(代理)联系;Delegate一般是一对一,而KVO和NSNotification可以一对多。

参考链接

KVC和KVO - 卢思豪

iOS–KVO的实现原理与具体应用 - 啊左

漫谈 KVC 与 KVO - SwiftCafe

Demo下载请点击这里