概述
kvo全称keyValueObserve也就是观察者模式,是apple提供的一套事件通知机制.允许对象监听另一个对应特殊属性的改变,并在改变时接受到该事件.一般继承自NSObject的对象都默认是支持kvo.
kvo和NSNotificationCenter都是ios中观察模式的一种实现.区别在于:
1.相对与被观察者和观察者之间的关系,kvo是一对一的,而不是一对多.就是说kvo监听到被观察者值改变时只会通知观察者,是一对一的关系.通知模式则是被观察者值改变的时候会发出全局通知,任何对应都可以接受到这个通知,是一对多的关系
2.kvo对被监听对象无侵入性,不需要修改其内部代码即可实现监听,而通知需要在被监听对象改变的时候添加发送通知代码
kvo的使用
1.注册观察者
/***************
@observer:观察者,也就是被观察者对象发色改变时通知的接收者
@keyPath:被观察者的属性名
@options:参数,options一般选择NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,这样当属性值发生改变时我们可以同时获得旧值和新值,如果我们只填NSKeyValueObservingOptionNew则属性发生改变时只会获得新值
@context:这个参数可传入任意类型的对象,这个值会传递到接受消息回掉的代码中,是kvo的一种传值方式.
**************/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
2.当所观察的属性发生改变时调用的函数
/********************
@keyPath:被观察者的属性名
@object:观察对象
@change:这是一个字典类型的值,通过键值对显示新的属性值和旧的属性值
@context:上面添加观察者时传递的信息
*******************/
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
3.移除观察者
/*******************
@object:观察对象
@keyPath:被观察者的属性名
*******************/
-(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
eg:
@interface Person : NSObject
@property(nonatomic,assign)NSInteger age;
@end
self.person = [[Person alloc] init];
self.person.age = 5;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
/ /注册观察者
[self.person addObserver:self forKeyPath:@"age" options:options context:@"Test"];
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent * )event{
self.person.age = 8;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"被观察者对象:%@,被观察的属性:%@,值的改变:%@\n,传递信息:%@",object,keyPath,change,context);
}
//打印结果
//被观察者对象:<Person: 0x600002568ab0>,
//被观察的属性:age,
//值的改变:{
// kind = 1;
// new = 8;
// old = 5;
//}
//,传递信息:Test
实现原理:
kvo是通过isa-swizzling技术实现的.在运行是根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类.并且将class方法中的,返回原类的class.
eg:
self.person1 = [[Person alloc] init];
self.person1.age = 5;
self.person2 = [[Person alloc] init];
self.person2.age = 6;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
//注册观察者
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"Test"];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.age = 8;
self.person2.age = 9;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"被观察者对象:%@,被观察的属性:%@,值的改变:%@\n,传递信息:%@",object,keyPath,change,context);
}
//打印结果
self.person1.age = 8;
self.person2.age = 9;
上面的例子中创建了两个Person对象person1和person2,监听person1的age属性而不监听person2.点击事件后同时改变person1和person2的age属性.其实在self.person1.age方法其实就是调用的set方法.通过打印person1和person的isa:
(lldb) po self.person1.isa
NSKVONotifying_Person
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) po self.person2.isa
Person
Fix-it applied, fixed expression was:
self.person2->isa
通过isa的结果我们可以看出,person1 的isa指针竟然指向NSKVONotifying_Person这个陌生的类,而person2的isa指针则还是正常的Person类.查看NSKVONotifying_Person类对象的superclass指针还是指向Person,所以我们可以得出NSKVONotifying_Person是Person的子类
(lldb) po ([NSKVONotifying_Person class]).superclass
Person
验证
1.打印类对象
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:)]);
2.打印结果
person1添加监听之前:- 0x106b03570 0x106b03570
person1添加监听之后:- 0x106e5e844 0x106b03570
然后我们使用LLDB打印一下0x106b03570和0x106e5e844这两个地址的IMP,我们把地址强制转化为IMP然后转化出来
(lldb) p (IMP)0x103540570
(IMP) $0 = 0x0000000103540570 (KVO的使用`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x10389b844
(IMP) $1 = 0x000000010389b844 (Foundation`_NSSetLongLongValueAndNotify)
这里我们就看清楚类
0x103540570这个地址的setAge:实现是调用Person类的setAge:方法,并且是在Person.m的第15行。
而0x10389b844这个地址的setAge:实现是调用_NSSetIntValueAndNotify这样一个C函数。
所以person2则没有发生变化,它一直是调用Person类的setAge:方法。而person1添加监听前后person1的setAge:方法发生了变化,添加监听前它是调用的Person类的setAge:方法,添加监听后变成了调用_NSSetIntValueAndNotify这样一个C函数
其实_NSSetIntValueAndNotify实质就是调用willChangeValueForKey和didChangeValueForKey方法,再这个2个方法中间再调用set方法赋值