KVO全称为Key Value Observing,键值监听机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,所以NSObject的子类都可使用该方法。
一、KVO使用步骤
1、注册观察者(为被观察者指定观察者以及被观察的属性)
创建一个YMPerson对象,有一个age属性,为age属性添加KVO监听
self.person1 = [[YMPerson alloc] init];
self.person1.age = 10;
/*
options: 有4个值,分别是:
NSKeyValueObservingOptionOld 把更改之前的值提供给处理方法
NSKeyValueObservingOptionNew 把更改之后的值提供给处理方法
NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。
*/
//注册一个监听器用于监听指定的key路径
[self.person addObserver:self forKeyPath:@"age" options:options context:@"context-age"];
⚠️:context参数值,对应回调方法里面的context
用途:如果在一个控制器中监听了多个属性的变化,而且每一个属性的变化之后对应的事件处理是不一样的,可以在注册观察者时通过context参数来区分判断
2、实现回调方法
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
3、修改需要监听的属性值,查看是否监听成功
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.age = 11;
}
4、注销观察者
[self.person1 removeObserver:self forKeyPath:@"age"];
二、KVO实现监听过程分析
疑问:在注册观察者之后,修改观察对象属性的值,就会走回调方法 上述过程 KVO是怎么实现的呢?
1、setAge方法修改属性
通过OC的消息机制知道:
调用 self.person1.age = 11; --> [self.person setAge:10] --> objc_msgsend(person,@selector(setAge))
控制台分别打印person1和person2的isa指针指向:
通过iOS底层原理-对象的本质和分类知道: 没有添加KVO监听实例对象self.person2.isa指针指向的是YMPerson的类对象; 使用了KVO监听的对象self.person1.isa指针指向的是NSKVONotifying_YMPerson的类对象,这个类其实就是YMPerson的一个子类,是Runtime动态创建的 新创建的NSKVONotifying_YMPerson继承于YMPerson,并且重写了setAge和class方法
代码模拟实现过程:
@implementation YMPerson
- (void)setAge:(int)age {
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 11;
}
打印结果:
2、验证
1)NSKVONotifying_YMPerson
self.person1 = [[YMPerson alloc] init];
self.person1.age = 10;
self.person2 = [[YMPerson alloc] init];
self.person2.age = 20;
NSLog(@"person1添加KVO监听之前 - %@ - %@",object_getClass(**self**.person1),object_getClass(self.person2));
// 给person1对象添加KVO监听
// runtime动态创建一个类 NSKVONotifying_YMPerson
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"context-age"];
NSLog(@"person1添加KVO监听之后 - %@ - %@",object_getClass(self.person1),object_getClass(self.person2));
打印结果:
2)继承自YMPerson,重写了class方法
代码验证:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 11;
self.person2.age = 22;
//这里调用class方法 返回的都是YMPerson类对象
NSLog(@"【instance class方法】 - %@ - %@",[_person1 class],[_person2 class]);
//获取类对象
Class personClass = object_getClass(self.person1);
//通过类对象获取父类
Class personClassFather = class_getSuperclass(personClass);
NSLog(@"类对象:%@ -- 父类:%@",personClass, personClassFather);
}
打印结果:
⚠️注意:这里不能用[YMPerson class]方法
因为 NSKVONotifying_YMPerson 重写了class方法,[YMPerson class]返回的并不是原对象而是原对象的父类也就是YMPerson类
如果没有重写的话,返回的YMPerson的isa指针指向的类应该是NSKVONotifying_YMPerson,
但是苹果官方不希望将NSKVONotifyin_YMPerson类的内部实现暴露出来,所以在内部重写了class方法,直接返回YMPerson类,所以我们在调用person的class方法时,返回的是Person类。
3)重写了setAge方法
从底层源码分析可知,NSKVONotifyin_Person中的setAge方法中其实调用了Foundation框架中C语言函数_NSSetIntValueAndNotify,而_NSSetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey方法,之后调用父类的setAge方法对成员变量赋值,最后调用didChangeValueForKey方法。其中didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法。
代码验证:
NSLog(@"person1添加KVO监听之前 - %p - %p",
[self.person1 methodForSelector: @selector(setAge:)],
[self.person2 methodForSelector: @selector(setAge:)]);
// 给person1对象添加KVO监听
// runtime动态创建一个类 NSKVONotifying_YMPerson
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"context-age"];
NSLog(@"person1添加KVO监听之后 - %p - %p",
[self.person1 methodForSelector: @selector(setAge:)],
[self.person2 methodForSelector: @selector(setAge:)]);
打印结果:
4)NSKVONotifyin_Person的内部结构
NSKVONotifyin_YMPerson作为YMPerson的子类,其superclass指针指向YMPerson类,并且NSKVONotifyin_YMPerson内部的setAge方法做了单独的实现。我们可以通过runtime的方法去分别打印person1和person2两个对象和NSKVONotifyin_YMPerson类对象内存储的对象方法:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor brownColor];
self.person1 = [[YMPerson alloc] init];
self.person1.age = 10;
self.person2 = [[YMPerson alloc] init];
self.person2.age = 20;
// 给person1对象添加KVO监听
// runtime动态创建一个类 NSKVONotifying_YMPerson
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"context-age"];
Class personClass1 = object_getClass(**self**.person1);
Class personClass2 = object_getClass(**self**.person2);
[self printMethodNamesOfClass:personClass1];
[self printMethodNamesOfClass:personClass2];
}
- (**void**)printMethodNamesOfClass:(Class)cls {
unsigned int count;
// 获得方法数据
Method *methodList = class_copyMethodList(cls, &count);
//存储方法名
NSMutableString *methodNames = [NSMutableString string];
//遍历所有方法
for (int i=0; i<count; i++) {
//获得方法
Method mtd = methodList[i];
//获得方法名
NSString *mtdName = NSStringFromSelector(method_getName(mtd));
//拼接方法名
[methodNames appendString:mtdName];
[methodNames appendString:@", "];
}
//释放
free(methodList);
//打印方法名
NSLog(@"%@: %@",cls,methodNames);
}
打印结果:
三、答疑解惑
-
1、NSKVONotifying_YMPerson的isa指向和YMPerson的class对象里的isa指针指向是否相同?
答:指向各自的元类对象meta-class
-
2、手动创建NSKVONotifying_YMPerson类后,运行后,KVO运行报错:[general] KVO failed to allocate class pair for name NSKVONotifying_YMPerson, automatic key-value observing will not work for this class
答:手动创建的类和运行时runtime动态创建的类冲突;可以删除或者取消勾选不参加编译
-
3、NSKVONotifying_Person怎么重写了setAge方法以实现触发回调? 答:
NSKVONotifyin_YMPerson中的setAge方法中其实调用了Fundation框架中C语言函数_NSsetIntValueAndNotify,而_NSsetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey方法,之后调用父类的setAge方法对成员变量赋值,最后调用didChangeValueForKey方法。其中didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法。 -
4、NSKVONotifyin_Person的内部结构是怎样的?
答:NSKVONotifying_YMPerson的内存结构及方法调用顺序
四、总结
1、KVO的本质是什么?
- 利用RuntimeAPI动态生成一个子类NSKVONotifying_A,并且让A的instance对象的isa指针指向这个全新的子类
- 当修改instance对象的属性时,会调用Foundation框架的_NSSetXXXCValueAndNotify函数:
1). willChangeValueForKey
2). 父类原来的setter方法
3). didChangeValueForKey
4).内部会触发监听器observe的监听方法observeValueForKeyPath:ofObject:change:context:
当我们给对象注册一个观察者添加了KVO监听时,系统会修改这个对象的isa指针指向。
在运行时,Runtime动态创建一个新的子类,NSKVONotifying_A类,将A的isa指针指向这个子类,来重写原来类的set方法;
set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
2、如何手动触发KVO?
手动调用willChangeValueForKey和didChangeValueForKey方法。
3、直接修改成员变量是否会触发KVO?
不会触发。因为KVO的本质是在调用对象的setter方法时,可以添加willChangeValueForKey和didChangeValueForKey实现手动触发
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person1 willChangeValueForKey:@"height"];
self.person1->_height = 85;
[self.person1 didChangeValueForKey:@"height"];
}
KVO本质分析Demo 密码: mdkb