概念
KVO的全称是Key-Value- Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。
本质
- 利用RuntimeAPI动态生成一个子类,并且让改instance对象的isa指向这个全新的子类
- 当修改对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
1.先调用willChangeValueForKey:
2.调用父类的setter方法
3.调用didChangeValueForKey:
- 内部触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
底层原理探索
先看以下一段简单的KVO的代码实现,观察RMPerson的属性age改变,简单的代码实现
#import "ViewController.h"
#import "RMPerson.h"
@interface ViewController ()
@property (nonatomic, strong) RMPerson *person1;
@property (nonatomic, strong) RMPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.person1 = [[RMPerson alloc] init];
self.person1.age = 10;
self.person2 = [[RMPerson alloc] init];
self.person2.age = 20;
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.person1 setAge:11];
[self.person2 setAge:21];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性发生了改变, options:%@ context:%@", object, keyPath, change, context);
}
- (void)dealloc
{
[self.person1 removeObserver:self forKeyPath:@"age"];
}
@end
--------------------------------------------------------------------------------------------------------------------------
//RMPerson.h
#import <Foundation/Foundation.h>
@interface RMPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@end
//RMPerson.M
#import "RMPerson.h"
@implementation RMPerson
- (void)setAge:(int)age
{
_age = age;
}
@end
在以上代码中,对象person1的属性age添加监听
和person2的属性age没有监听
作对比,可以猜测,两者调用的都是对象的setAge:方法
,为何添加了KVO监听的peron1
会回调方法observeValueForKeyPath:ofObject:change:context:
,而person2
却没有。
猜测:
- 1.苹果调用
setAge:
上做了手脚 - 2.会不会是生成了一个新的中间对象或者利用OC独有的运行时状态,生成了一个RMPerson的子类,重写了age的setter方法?
带着上面两点疑问,打印下监听前后person1
和person2
的类对象是否有变化,看以下代码
1、监听前后类对象的变化
NSLog(@"监听前 %@:%p %@:%p",
object_getClass(self.person1),object_getClass(self.person1),
object_getClass(self.person2),object_getClass(self.person2));
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"监听后 %@:%p %@:%p",
object_getClass(self.person1),object_getClass(self.person1),
object_getClass(self.person2),object_getClass(self.person2));
//打印结果
2018-08-24 17:40:19.689016+0800 KVO[60640:4402657] 监听前 RMPerson:0x10e2600b0 RMPerson:0x10e2600b0
2018-08-24 17:40:20.621251+0800 KVO[60640:4402657] 监听后 NSKVONotifying_RMPerson:0x608000112750 RMPerson:0x10e2600b0
从上面代码看出,
1.监听前两者都是RMPerson类,其类对象地址是一致的
2.给对象person1的属性age添加监听后,对象person1的打印的类是NSKVONotifying_RMPerson
,
所以,我们可以得出给对象属性添加监听后,苹果会动态的生成一个NSKVONotifying_XXX
的中间类,其继承于RMPerson,如何得知呢?打印一下NSKVONotifying_RMPerson
的父类 [object_getClass(self.person1) superClass]
即可知道其父类是RMPerson。
2、监听前后其"setAge:方法地址"
打印下添加监听后其setAge:
方法地址
NSLog(@"person1添加KVO监听之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
2018-08-27 11:02:25.651418+0800 KVO[72155:5132928] person1添加KVO监听之后 - 0x100669f8e 0x1002c4520
(lldb) p (IMP)0x100669f8e
(IMP) $0 = 0x0000000100669f8e (Foundation`_NSSetIntValueAndNotify)
(lldb) p (IMP)0x1002c4520
(IMP) $1 = 0x00000001002c4520 (KVO`-[RMPerson setAge:] at RMPerson.m:13)
(lldb)
从上面,打印后,使用lldb打印其地址的内容,可以得知,监听对象person1的属性age后,其setAge:
方法底层实际是调用了_NSSetIntValueAndNotify
方法,此方法的伪代码如下
- (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];
}
要了解其_NSSetIntValueAndNotify
其方法的实现,要反编译其Foundation
框架的底层实现,有兴趣的自己再了解。从上面我们可看出,setAge:
方法实际实现了此三个方法
① willChangeValueForKey:
② [super setAge:age]
③ didChangeValueForKey
最后再通知监听器,告诉其某某属性值发生改变了,回调了[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
此方法。
总结
使用KVO监听了某个对象的属性,实际就是
- 利用RuntimeAPI动态生成一个子类,并且让改instance对象的isa指向这个全新的子类
- 当修改对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
1.先调用willChangeValueForKey:
2.调用父类的setter方法
3.调用didChangeValueForKey:
- 内部触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
为何称KVO为黑魔法,实际上掩饰了利用RuntimeAPI动态生成一个子类,属性值发生了改变时,修改父类的值得过程