一 、 基本使用
概述
KVO全称NSKeyValueObserving,是一个非正式协议,它定义了对象之间观察和通知状态改变的通用机制的。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
在 Objective-C 和 Cocoa 中,有许多事件之间进行通信的方式,并且每个都有不同程度的形式和耦合
-
NSNotification & NSNotificationCenter 提供了一个中央枢纽,一个应用的任何部分都可能通知或者被通知应用的其他部分的变化。唯一需要做的是要知道在寻找什么,主要是通知的名字。例如,UIApplicationDidReceiveMemoryWarningNotification 是给应用发了一个内存不足的信号。
-
Key-Value Observing 允许 ad-hoc,通过在特定对象之间监听一个特定的 keypath 的改变进行事件内省。例如:一个 ProgressView 可以观察 网络请求的 numberOfBytesRead 来更新它自己的 progress 属性。
-
Delegate 是一个流行的传递事件的设计模式,通过定义一系列的方法来传递给指定的处理对象。例如:UIScrollView 每次它的 scroll offset 改变的时候都会发送 scrollViewDidScroll: 到它的代理
-
Callbacks 不管是像 NSOperation 里的 completionBlock(当 isFinished==YES 的时候会触发),还是 C 里边的函数指针,传递一个函数钩子比如 SCNetworkReachabilitySetCallback(3)。
KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。
下面编写一段代码感受一下
//
// KVOPerson.h
// KVODemo
//
// Created by tinghou on 2018/11/14.
// Copyright © 2018年 tinghhout. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface KVOPerson : NSObject
@property (assign, nonatomic) int age;
@property (assign, nonatomic) int height;
@end
添加监听
self.person1 = [[KVOPerson alloc] init];
self.person1.age = 1;
self.person1.height = 11;
self.person2 = [[KVOPerson alloc] init];
self.person2.age = 2;
self.person2.height = 22;
// 给对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self.person1 addObserver:self forKeyPath:@"height" options:options context:@"456"];
改变值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 2;
self.person2.age = 12;
self.person1.height = 3;
self.person2.height = 23;
}
观察改变
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
运行程序,我们就可以看到结果了,监听到了新值和旧值
总结
使用KVO分为三个步骤:
- 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
- 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
- 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。
二 、 本质分析
上面代码创建两个Person对象person1和person2,监听person1的age属性而不监听person2,然后改变person1和person2的age属性。
现在我们打印一下person类,当添加kvo监听之后打印两个对象
NSLog(@"person1添加KVO监听之后:-%@ %@", object_getClass(self.person1), object_getClass(self.person2));
然后打印的结果是这样的:
2018-11-16 20:40:09.282241+0800 KVODemo[32712:1011869] libMobileGestalt MobileGestalt.c:890: MGIsDeviceOneOfType is not supported on this platform.
2018-11-16 20:40:43.134227+0800 KVODemo[32712:1011869] person1添加KVO监听之后:-NSKVONotifying_KVOPerson KVOPerson
或者我们打印person对象的isa指针
(lldb) po self.person1.isa
NSKVONotifying_KVOPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) po self.person2.isa
KVOPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
发现上面person1添加了kvo监听之后产生了一个 NSKVONotifying_KVOPerson 类,而person2没有添加kvo监听就是原类。现在我们在person类里面添加这些代码
//
// KVOPerson.m
// KVODemo
//
// Created by tinghou on 2018/11/14.
// Copyright © 2018年 tinghhout. All rights reserved.
//
#import "KVOPerson.h"
@implementation KVOPerson
- (void)setAge:(int)age
{
_age = age;
NSLog(@"setAge:");
}
//- (int)age
//{
// return _age;
//}
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
再次运行看到结果打印是这样的
2018-11-16 20:46:00.552063+0800 KVODemo[32822:1033907] willChangeValueForKey
2018-11-16 20:46:00.552197+0800 KVODemo[32822:1033907] setAge:
2018-11-16 20:46:00.552260+0800 KVODemo[32822:1033907] didChangeValueForKey - begin
2018-11-16 20:46:00.552416+0800 KVODemo[32822:1033907] 监听到<KVOPerson: 0x600000304a10>的age属性值改变了 - {
kind = 1;
new = 2;
old = 1;
} - 123
2018-11-16 20:46:00.552488+0800 KVODemo[32822:1033907] didChangeValueForKey - end
通过上面的打印顺序我们看到:
- 1.首先调用willChangeValueForKey:方法。
- 2.然后调用setAge:方法真正的改变属性的值。
- 3.开始调用didChangeValueForKey:这个方法,调用[super didChangeValueForKey:key]时会通知监听者属性值已经改变,然后监听者执行自己的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context这个方法。
由此猜测 person类添加kvo之后动态产生的类NSKVONotifying_KVOPerson大致逻辑是这样的:
在NSKVONotifying_KVOPerson这个子类的setAge:方法中主要是实现了一个C方法_NSSetIntValueAndNotify(),这个方法的实现分三步,首先是属性将要改变时调用willChangeValueForKey:,然后是调用父类即Person类的setAge:方法来真正的改变age属性的值,当age属性的值改变完成之后再调用didChangeValueForKey:这个方法来通知监听者属性值已经改变。下面是模拟它的伪代码:
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
现在查看添加监听前后setAge:方法的实现来验证
- (IMP)methodForSelector:(SEL)aSelector;这个方法是传入一个selector返回一个方法的实现即imp,这里我们打印一下person1添加监听前后person1和person2的setAge:方法的实现的地址来判断这两个对象调用的的setAge:方法是否发生了改变:
NSLog(@"person1添加监听之前:- %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"测试信息"];
NSLog(@"person1添加监听之后:- %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
打印结果:
person1添加监听之前:- 0x10f5c84d0 0x10f5c84d0
person1添加监听之后:- 0x10f96df8e 0x10f5c84d0
然后我们使用LLDB打印一下0x10f5c84d0和0x10f96df8e这两个地址的IMP,我们把地址强制转化为IMP然后转化出来:
(lldb) p (IMP)0x10678a4d0
(IMP) $0 = 0x000000010678a4d0 (KVODemo`-[KVOPerson setAge:] at Person.m:13)
(lldb) p (IMP)0x106b2ff8e
(IMP) $1 = 0x0000000106b2ff8e (Foundation`_NSSetIntValueAndNotify)
这样我们就看的很清晰了。
0x10678a4d0这个地址的setAge:实现是调用KVOPerson类的setAge:方法,并且是在KVOPerson.m的第13行。
而0x106b2ff8e这个地址的setAge:实现是调用_NSSetIntValueAndNotify这样一个C函数。
所以person2则没有发生变化,它一直是调用KVOPerson类的setAge:方法。而person1添加监听前后person1的setAge:方法发生了变化,添加监听前它是调用的KVOPerson类的setAge:方法,添加监听后变成了调用_NSSetIntValueAndNotify这样一个C函数。
由此得出KVO的本质是这样的:
- 利用RuntimeAPI动态生成一个子类叫做 NSKVONotifying_KVOPerson,然后KVO会在这个派生类中,重写基类中任何被观察属性的setter方法,在setter方法中实现真正的通知机制,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数,进而触发
willChangeValueForKey:
父类原来的setter
didChangeValueForKey: - 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
- (void)setAge:(int)age
{
[super setAge:age];
[监听者 observeValueForKeyPath:@"age" ofObject:self change:@{} context:nil];
}
由此还可以手动实现键值观察:
/**
首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;
其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
*/
// for manual KVO - age 手动实现键值观察
- (int) age
{
return age;
}
- (void) setAge:(int)theAge
{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
由此得出使用了KVO监听的person对象是这样的:
未用了KVO监听的person对象是这样的:
参考文章 Key-Value Observing