内容简介:例如,我们定义一个 YZPerson 类 继承自 NSObject ,里面有name 和 age 两个属性然后在ViewController中,写如下代码执行之后结果为
- KVO全称KeyValueObserving,俗称 键值监听 ,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
- KVC和KVO都属于键值编程而且底层实现机制都是 isa-swizzing 。
- KVO和NSNotificationCenter都是iOS中 观察者模式 的一种实现。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
- KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。
实现原理
- KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。
- 在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。当修改 instance 对象的属性时,会调用 Foundation框架的 _NSSetXXXValueAndNotify 函数 ,该函数里面会先调用 willChangeValueForKey: 然后调用父类原来的 setter 方法修改值,最后是 didChangeValueForKey:。didChangeValueForKey 内部会触发监听器(Oberser)的监听方法observeValueForKeyPath:ofObject:change:context:
- 并且将class方法重写,返回原类的Class。
KVO的使用
使用方法
- 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
- 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
- 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。
例如,我们定义一个 YZPerson 类 继承自 NSObject ,里面有name 和 age 两个属性
@interface YZPerson : NSObject @property (nonatomic ,assign) int age; @property (nonatomic,strong) NSString *name; @end 复制代码
然后在ViewController中,写如下代码
- (void)viewDidLoad {
[super viewDidLoad];
//调用方法
[self setNameKVO];
}
-(void)setNameKVO{
self.person = [[YZPerson alloc] init];
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = @"ccc";
}
-(void)dealloc
{
// 移除监听
[self.person removeObserver:self forKeyPath:@"name"];
}
复制代码
执行之后结果为
KVOdemo[11482:141804] 监听到<YZPerson: 0x6000004e8400>的name属性值改变了 - {
kind = 1;
new = ccc;
old = "<null>";
} - 1111- 1111
复制代码
注意点
需要注意的是,上面代码中我们已经移除了监听,如果再次移除的话,就会crash
例如
- (void)viewDidLoad {
[super viewDidLoad];
//调用方法
[self setNameKVO];
}
-(void)setNameKVO{
self.person = [[YZPerson alloc] init];
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
// 移除监听
[person removeObserver:self forKeyPath:@"name"];
// 再次移除
[person removeObserver:self forKeyPath:@"name"];
}
复制代码
移除多次会报错
KVOdemo[9261:2171323] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <ViewController 0x139e07220> for the key path "name" from <YZPerson 0x281322f20> because it is not registered as an observer.' 复制代码
如果忘记移除的话,有可能下次收到这个属性的变化的时候,会carsh
所以,我们要保证add和remove是成对出现的
抛出疑问
加入我们又两个YZPerson对象,只监听其中一个
- (void)viewDidLoad {
[super viewDidLoad];
[self setNameKVO];
}
-(void)setNameKVO{
self.person = [[YZPerson alloc] init];
self.person2 = [[YZPerson alloc] init];
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.name = @"ccc";
self.person2.name = @"ddd";
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc
{
// 移除监听
[self.person removeObserver:self forKeyPath:@"name"];
}
复制代码
点击屏幕时候,打印如下
监听到<YZPerson: 0x600001afa740>的name属性值改变了 - {
kind = 1;
new = ccc;
old = "<null>";
} - 1111
复制代码
但是我们知道,
self.person.name = @"ccc"; self.person2.name = @"ddd"; 复制代码
上面这两句代码都是调用 setName
@implementation YZPerson
- (void)setName:(NSString *)name{
_name = name;
}
复制代码
也就是说,两个对象,都是调用 setName 方法,根据iOS的机制,应该都是根据 YZPerson的isa指针,去类对象中查找方法。怎么就能做到 self.person.name 可以监听 self.person2.name 不能监听呢?
本质分析
针对上面的疑问,我们可以猜测,是不是person 和 person2 的isa指针不一样呢,导致执行的方法不同呢?
打断点,并打印两者的isa
(lldb) po self.person->isa NSKVONotifying_YZPerson (lldb) po self.person2->isa YZPerson 复制代码
发现果然是isa指针不同,既然isa指向不同了。是不是说明两者的类对象不同呢?答案是肯定的。因为oc中,就是根据isa去查找类对象的,那么接下来进行验证
验证
对类对象进行验证
导入 runtime,对两者的类进行打印
NSLog(@"person添加KVO监听之前 - %@ %@",
object_getClass(self.person),
object_getClass(self.person2));
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
NSLog(@"person添加KVO监听之后 - %@ %@",
object_getClass(self.person),
object_getClass(self.person2));
复制代码
打印结果为
KVOdemo[13302:171740] person添加KVO监听之前 - YZPerson YZPerson KVOdemo[13302:171740] person添加KVO监听之后 - NSKVONotifying_YZPerson YZPerson 复制代码
由此可见,添加KVO监听之后,确实 self.person 的类对象是NSKVONotifying_YZPerson 而self.person2的类对象不变,依然是 YZPerson
注意点:如果使用 [self.person class] 无法获取真实的类
例如我们在添加KVO监听之后,这样来获取类对象
NSLog(@"person添加KVO监听之后 - %@ %@",
[self.person class],
[self.person2 class]);
复制代码
那么打印结果为
KVOdemo[17839:239214] person添加KVO监听之后 - YZPerson YZPerson 复制代码
这是因为,苹果为我们生成了中间类 NSKVONotifying_YZPerson 但是,他并不想让我们知道有这个类的存在,重写了这个 NSKVONotifying_YZPerson 的class方法,所以,我们获取的结果是不准确的。
对方法IMP进行验证
我们知道,当改变name属性的时候,是调用setName: 进行的,那我们就来查看一下setName: 有什么变化
NSLog(@"person添加KVO监听之前 - %p %p",
[self.person methodForSelector:@selector(setName:)],
[self.person2 methodForSelector:@selector(setName:)]);
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
NSLog(@"person添加KVO监听之后 - %p %p",
[self.person methodForSelector:@selector(setName:)],
[self.person2 methodForSelector:@selector(setName:)]);
复制代码
结果为
KVOdemo[13655:177448] person添加KVO监听之前 - 0x10ccfa630 0x10ccfa630 KVOdemo[13655:177448] person添加KVO监听之后 - 0x10d056d1a 0x10ccfa630 复制代码
有上面打印结果可知,添加监听之后,self.person的 setName 地址变了。继续通过LLDB查看
KVOdemo[13655:177448] person添加KVO监听之前 - 0x10ccfa630 0x10ccfa630 KVOdemo[13655:177448] person添加KVO监听之后 - 0x10d056d1a 0x10ccfa630 (lldb) p (IMP)0x10ccfa630 (IMP) $0 = 0x000000010ccfa630 (KVOdemo`-[YZPerson setName:] at YZPerson.m:12) (lldb) p (IMP)0x10d056d1a (IMP) $1 = 0x000000010d056d1a (Foundation`_NSSetObjectValueAndNotify) 复制代码
可知,添加KVO监听之后,setName:方法指向了 Foundation 框架中的 _NSSetObjectValueAndNotify
元类对象验证
既然添加KVO监听之后,类对象不是同一个,那元类对象呢?如下验证
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
NSLog(@"类对象 - %@ %@",
object_getClass(self.person), // self.person.isa
object_getClass(self.person2)); // self.person2.isa
NSLog(@"元类对象 - %@ %@",
object_getClass(object_getClass(self.person)), // self.person.isa.isa
object_getClass(object_getClass(self.person2))); // self.person2.isa.isa
复制代码
结果为
KVOdemo[13655:177448] 类对象 - NSKVONotifying_YZPerson YZPerson KVOdemo[13655:177448] 元类对象 - NSKVONotifying_YZPerson YZPerson 复制代码
可知,元类对象变成了 NSKVONotifying_YZPerson
内部调用流程
那设置了kvo监听之后,内部调用有什么流程呢?我们在Person中添加如下代码
#import "YZPerson.h"
@implementation YZPerson
- (void)setName:(NSString *)name{
_name = name;
}
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
复制代码
点击屏幕之后,如下打印
KVOdemo[17486:233248] willChangeValueForKey
KVOdemo[17486:233248] didChangeValueForKey - begin
KVOdemo[17486:233248] 监听到<YZPerson: 0x600000889ca0>的name属性值改变了 - {
kind = 1;
new = ccc;
old = "<null>";
} - 1111
KVOdemo[17486:233248] didChangeValueForKey - end
复制代码
也就是说调用 [super didChangeValueForKey:key]; 的时候,监听到监听对象的改变,进而处理监听逻辑
窥探 NSKVONotifying_YZPerson 的方法
- (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 method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
-(void)setNameKVO{
self.person = [[YZPerson alloc] init];
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
NSLog(@"person添加KVO监听之后 - self.person的类是:%@ 里面的方法有:",object_getClass(self.person));
[self printMethodNamesOfClass:object_getClass(self.person)];
}
复制代码
上面代码执行结果为
KVOdemo[19286:259546] person添加KVO监听之后 - self.person的类是:NSKVONotifying_YZPerson 里面的方法有: KVOdemo[19286:259546] NSKVONotifying_YZPerson setName:, class, dealloc, _isKVOA, 复制代码
这也进一步验证了,系统重写了新建的子类 NSKVONotifying_YZPerson 的setName, class, dealloc,新增了 _isKVOA方法
手动调用KVO
由上面可知,KVO监听的关键 willChangeValueForKey 和 didChangeValueForKey 起了关键作用,一般来说只有监听属性发生变化的时候,才能触发监听,但是如果我们想自己手动调用KVO的话,只要自己手动调用这两个方法就可以了。eg:
-(void)setNameKVO{
self.person = [[YZPerson alloc] init];
// 注册观察者
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
NSLog(@"person添加KVO监听之后 - self.person的类是:%@ 里面的方法有:",object_getClass(self.person));
[self printMethodNamesOfClass:object_getClass(self.person)];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// self.person.name = @"ccc";
// 手动调用KVO
[self.person willChangeValueForKey:@"name"];
[self.person didChangeValueForKey:@"name"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc
{
// 移除监听
[self.person removeObserver:self forKeyPath:@"name"];
}
复制代码
每次点击屏幕的时候,打印如下
监听到<YZPerson: 0x600003e5b020>的name属性值改变了 - {
kind = 1;
new = "<null>";
old = "<null>";
} - 1111
复制代码
可以看到虽然,new 和 old都是null ,也就是name的值没有改变,但是因为我们手动调用了,
[self.person willChangeValueForKey:@"name"];
[self.person didChangeValueForKey:@"name"];
复制代码
所以就是会触发KVO
拓展深入
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数 willChangeValueForKey: 父类原来的setter didChangeValueForKey: 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
如何手动触发KVO?
- 手动调用willChangeValueForKey:和didChangeValueForKey:
直接修改成员变量会触发KVO么?
- 不会触发KVO
因为,触发KVO是因为,执行set方法时候,调用 willChangeValueForKey didChangeValueForKey 但是直接修改成员变量不会调用set方法
eg: 我们把name 成员变量 设置为如下的形式,就不会自动生成set 和 get方法
@interface YZPerson : NSObject
{
@public
NSString *_name;
}
@property (nonatomic ,assign) int age;
@end
复制代码
在监听控制器里面,改成如下操作,直接修改成员变量
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person->_name = @"abc";
}
复制代码
这样是不会触发KVO的,如果我们想让它触发KVO,就手动调用,如下
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person willChangeValueForKey:@"name"];
self.person->_name = @"abc";
[self.person didChangeValueForKey:@"name"];
}
@end
复制代码
这样就可以触发KVO了。
通过KVC修改属性会触发KVO么?
- 会触发KVO
- 详细分析见KVC那点儿事
KVC 与 KVO 的不同?
KVC(键值编码),即 Key-Value Coding,一个非正式的 Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用 Setter、Getter 方法等显式的存取方式去访问。 KVO(键值监听),即 Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了 setter 方法、或者使用了 KVC 赋值。
KVO和 notification(通知)的区别?
notification 比 KVO 多了发送通知的一步。 两者都是一对多,但是对象之间直接的交互,notification 明显得多,需要notificationCenter 来做为中间交互。而 KVO 如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了,只留一句“交由系统通知”,具体的可参照以上实现过程的剖析。
notification 的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。 (参照通知机制第五节系统通知名称内容)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript Patterns
Stoyan Stefanov / O'Reilly Media, Inc. / 2010-09-21 / USD 29.99
What's the best approach for developing an application with JavaScript? This book helps you answer that question with numerous JavaScript coding patterns and best practices. If you're an experienced d......一起来看看 《JavaScript Patterns》 这本书的介绍吧!