KVO全称为Key Value Observing,键值监听机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,所以NSObject的子类都可使用该方法。
KVO使用步骤
1.注册观察者(为被观察这指定观察者以及被观察者属性)
创建一个Person对象,写一个age属性,为age属性添加KVO监听
/*
options: 有4个值,分别是:
NSKeyValueObservingOptionOld 把更改之前的值提供给处理方法
NSKeyValueObservingOptionNew 把更改之后的值提供给处理方法
NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。
*/
//注册一个监听器用于监听指定的key路径
[self.person addObserver:self forKeyPath:@"age" options:options context:@"123"];
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 = 10;
}
使用注意点
[self.person addObserver:self forKeyPath:@"age" options:options context:@"123"] ;这行代码中context:@"123"
这个参数传的值都会
-- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{}传到回调方法中context
中来,用途: 如果说在一个控制器中我们监听了多个属性的变化,而每一个属性的变化之后对应的事件处理也是不一样的,那么在注册观察者时context 传不同的字符串就可以区分判断了
思考:为什么注册观察者之后,修改观察对象属性的值,就会走回调方法?KVO是怎么实现这个功能的呢?
带着这两个问题下面来一探究竟
- OC是消失机制当我们调用 self.person1.age = 10; 这行代码时实际就是[self.person setAge:10] 然后再转化成objc_msgsend(person,@selector(setAge)),
那么是不是KVO在setAge 这个方法中做了文章呢?
前几篇文章已经写到对象方法存储到类对象中,那么在控制台输入 po self.person.isa 发现结果并不是 Person 而是 NSKVONotifying_Person 这说明KVO在运行时新建了一个NSKVONotifying_Person类,将person的isa指针指向这个类
, 在控制台输入 po [self.person class]发现输出的是Person
而不是NSKVONotifying_Person
。这是为什么?
猜测:系统在运行时,KVO动态创建一个NSKVONotifying_Person类,将person的isa指针指向这个类。新创建的NSKVONotifying_Person 继承与Person 并且重写了 setAge方法和 class方法
验证1:NSKVONotifying_Person是否继承自Person,是否重写了Class方法
#import <objc/runtime.h>
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 20;
//获取类对象
Class personClass = object_getClass(self.person1);
//通过类对象获取父类
Class personClass02 = class_getSuperclass(personClass);
NSLog(@"%@-------%@",personClass02,personClass03);
}
输出 NSKVONotifying_Person-------Person
通过这几行代码就可以证明NSKVONotifying_Person 继承自 Person
- 细节
再获取类对象时可以使用object_getClass 也可以使用[A class],上面为什么使用了object_getClass而不是使用[A class]呢?
答案: NSKVONotifying_Person 重写了class方法,[person class]返回的并不是原对象而是原对象的父类也就是Person类,如果没有重写的话,返回person的isa指针指向的类打印结果应该是NSKVONotifyin_Person,但是苹果官方不希望将NSKVONotifyin_Person类的内部实现暴露出来,所以在内部重写了class方法,直接返回Person类,所以我们在调用person的class方法时,返回的是Person类。
验证2:NSKVONotifying_Person是否重写了setAge方法
//新建另外一个person2对象,不添加监听
self.person2 = [[Person alloc] init];
self.person2.age = 2;
//点击修改person2的值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.age = 20;
Class personClass = object_getClass(self.person);
Class personClass02 = object_getClass(personClass);
Class personClass03 = class_getSuperclass(personClass);
//修改person2的值
self.person2.age = 20;
}
答案:person2 在调用setAge方法时并没有触发回调方法,而添加了观察者的person对象在调用setAge 方法时触发了方法,说明NSKVONotifying_Person 重写了setAge方法
思考1:NSKVONotifying_Person怎么重写了setAge方法以实现触发回调
经过查看底层源码和相关资料分析们可以知道,NSKVONotifyin_Person
中的setAge
方法中其实调用了Fundation
框架中C语言函数_NSsetIntValueAndNotify
,而_NSsetIntValueAndNotify
内部做的操作相当于,首先调用willChangeValueForKey
方法,之后调用父类的setAge方法对成员变量赋值,最后调用didChangeValueForKey
方法。其中didChangeValueForKey
中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath
方法。
思考2:NSKVONotifyin_Person的内部结构是怎样的?
NSKVONotifyin_Person
作为Person
的子类,其superclass
指针指向Person
类,并且NSKVONotifyin_Person
内部的setAge方法做了单独的实现。我们可以通过runtime
的方法去分别打印person1
和person2
两个对象和NSKVONotifyin_Person
类对象内存储的对象方法:
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self printMethods: object_getClass(self.person1)];
[self printMethods: object_getClass(self.person2)];
[self.person1 removeObserver:self forKeyPath:@"age"];
}
- (void) printMethods:(Class)cls
{
unsigned int count ;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ : ", cls];
for (int i = 0 ; i < count; i++) {
Method method = methods[I];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString: methodName];
[methodNames appendString:@" "];
}
NSLog(@"%@",methodNames);
free(methods);
}
打印输出:
Person : setAge:, age,
NSKVONotifying_Person : setAge:, class, dealloc, _isKVOA,
NSKVONotifyin_Person的内存结构及方法调用顺序
NSKVONotifyin_Person的内存结构及方法调用顺序图解
总结:
1、KVO的本质是什么?
当我们给对象注册一个观察者添加了KVO监听时,系统会修改这个对象的isa指针指向。在运行时,动态创建一个新的子类,NSKVONotifying_A类,将A的isa指针指向这个子类,来重写原来类的set方法;set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
2、如何手动触发KVO?
实现调用willChangeValueForKey和didChangeValueForKey方法。
如有疑问:
iOS OC对象的本质窥探(一)
iOS OC对象的本质窥探(对象分类)(二)
特别推荐:
iOS获取手机唯一标示
iOS 高德地图实现大头针展示,分级大头针,自定制大头针,在地图上画线,线和点共存,路线规划(驾车路线规划),路线导航,等一些常见的使用场景
网友评论