1、KVO的概念
- 概念:KVO--Key-Value Observing,又称“键值观察(监听)”,用于监听一个对象的某个属性的改变。
2、KVO的如何使用?
- 那么,KVO应该如何使用呢?举个例子:我们想监听某个人(对象)的姓名(属性)变更,如果变更了就作出响应,怎么做呢?
1.创建一个Person类,内有部分属性比如: name
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) NSInteger age;
@end
2.创建Person实例,并且监听该实例的某属性
#import "Person.h"
@interface ViewController ()
@property(nonatomic, strong) Person *aPerson;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.aPerson = [[Person alloc] init];
self.aPerson.name = @"小华";
//监听属性name
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.aPerson addObserver:self forKeyPath:@"name" options:options context:nil];
}
@end
注:这里需要注意第二个参数 keyPath 需要手动键入字符串类型的属性名,要仔细防止手误。
3.添加监听响应的方法,响应条件为当对象的属性值发生改变时就会响应
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性发生改变 -> %@", object, keyPath, change);
}
4.对目标对象的属性值进行修改
- (void)changePropertyNameValue {
self.aPerson.name = @"小明";
}
此方法执行后就会自动调用步骤3的方法。
5.移除监听
- (void)dealloc {
[self.aPerson removeObserver:self forKeyPath:@"name"];
}
整个流程下来,基本就是实现KVO的整个步骤。 在修改属性值后控制台输出为:
3、KVO的实质是什么?或者说原理?
要看清KVO的实质,不妨先来看看在对某对象的某属性进行键值观察前后有什么区别吧 思路:我们创建Person的两个实例对象,一个personA,一个personB并且对personB的name进行键值观察,然后打印两者的isa指针,看下两者的区别
1.分别创建两个对象personA和personB,并且对后者的属性name KVO。
//创建实例
self.personA = [[Person alloc] init];
self.personA.name = @"小华";
self.personB = [[Person alloc] init];
self.personB.name = @"小华KVO";
//监听属性name
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.personB addObserver:self forKeyPath:@"name" options:options context:nil];
2.查看personA和personB的isa区别
通过对比控制台的输出,我们大致可以看出一些端倪,就是两者 isa 指针的指向略有不同,
我们再用object_getClassName(instance)查看 personA 和 personB 实例区别:
注:通过object_getClass函数拿到的才是isa真正指向的类。
使用 -(IMP)methodForSelector:@selector() 来获取两者实例的实现地址:
没看错的话,添加了 KVO 的 PersonB 在调用 setName: 的时候,调用了 Foundation 的 _NSSetObjectValueAndNotify 方法。
注:methodForSelector:@selector()用来获取某方法的实现地址
那么我们可以通过下图描述在对一个实例添加了 KVO 之后以及修改了被监听的属性值之后(调用setProperty:)这一系列操作:
对于未使用KVO监听的实例对象personA:
对于使用了KVO监听的实例对象personB:
NSKVONotifying_Person 类是利用runtime动态特性的API创建出来的一个 Person 的一个子类,那么,在我们调用 personB 的 setName:时,实际上是调用了 NSKVONotifying_Person 的 _NSSetObjectValueAndNotify()函数的实现,此函数的实现可以参考以下伪代码:
- (void)setName:(NSString *)name {
_NSSetObjectValueAndNotify();
}
void _NSSetObjectValueAndNotify() {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
- (void)didChangeValueForKey:(NSString *)key {
// 通知监听器,name属性发生了改变
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
以上,基本就是 KVO 的实质。
4、如何触发KVO?
以下有8个方法
外部修改 name:
// 1
self.personB.name = @"小明";
// 2
[self.personB setValue:@"小明2" forKey:@"name"];
// 3
[self.personB setValue:@"小明3" forKey:@"_name"];
内部修改 name:
// 4
self.name = @"小明4";
// 5
_name = @"小明5";
// 6
[self setValue:@"小明6" forKey:@"name"];
// 7
[self setValue:@"小明7" forKey:@"_name"];
通过执行结果怎样呢?
1、2、4、6 会触发监听响应,而3、5、7不会触发监听响应。
那么,原因为何呢?
因为在上面我们知道,KVO的本质是通过runtime的isa-swizzling新建了一个子类,并且在 setProperty 的时候调用了这个子类的 _NSSetXXXValueAndNotify() 方法,因为1、2、4、6会走setter方法,而3、5、7直接设置成员变量 name 的话,并没有走setter方法,所以也就无法触发监听。
我们在使用KVC时,key加不加_决定了是否走setter方法。
另外,数组如何去监听呢?
正常KVO监听数组时,即使我们修改了数组的内容(这里指修改数组内部的内容-增删改)因为没有触发setter方法所以不会触发监听,最简单的就是加一句self.arr = self.arr就可以触发监听了。
5、怎样可以手动触发 KVO 响应?
如果一个类想要实现手动的 KVO 监听和响应,则必须重写NSObject实现的automaticallyNotifiesObserversForKey:方法,并对需要实现手动发送的key返回NO,其余则调用super。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"property"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
然后在 property 的值改变之前调用 willChangeValueForKey:,在值改变之后调用 didChangeValueForKey:。当然,在什么样的情况下才调用这两个方法,是由我们程序员(或者说是需求场景)的逻辑决定的。
如果一个操作造成了多个key的值的改变,则willChangeValueForKey:和didChangeValueForKey:必须嵌套着调用,示例如下:
- (void)setProperty:(id)theProperty {
[self willChangeValueForKey:@"theProperty"];
[self willChangeValueForKey:@"theProperty2"];
_theProperty = theProperty;
_theProperty2 = _theProperty2+1;
[self didChangeValueForKey:@"theProperty2"];
[self didChangeValueForKey:@"theProperty"];
}
willChangeValueForKey:和didChangeValueForKey:调用时都会调用 valueForKey:,并把得到的结果分别当成old value和new value,以告知观察者,猜测系统就是用看这两个方法来发送通知的。
6、小结
- 1.KVO的实质
KVO- Key-Value Observing 是利用addObserver:forKeyPath:options:context:在对一个需要观察的属性property注册一个观察者以后,利用Runtime的isa-swizzling新建了一个该对象class的子类 NSKVONotifying_ObjectClass,
并将该对象的isa指针指向这个子类,在调用该对象的 setPropertyName: 方法的时候,会调用这个子类的_NSSetXXXValueAndNotify()方法,在此方法里,分别调用willChangeValueForKey: 、父类的setPROPERTYNAME:方法和didChangeValueForKey: 。
- 2._NSSet**ValueAndNotify()的实现:
[self willChangeValueForKey:@"PROPERTY_NAME"];(该方法会发出通知)
[super setPROPERTY_NAME:newPropertyValue];
[self didChangeValueForKey:PROPERTY_NAME];(该方法会发出通知)
- 3.几个有用的基础的方法
利用
object_getClassName(instance)可以获取 instance 的 isa真正的指向(实例对象的isa指向的是类)。
对前文例子利用 LLDB 调试,若 po objet_getClassName(self.personB) 得到的是 NSKVONotifyingPerson。
利用
[instance methodForSelector:@selector()]可以获取到某实例在调用某方法时该方法的实现地址IMP。
对前文的例子利用 LLDB 调试,若 po [self.personB methodForSelector:@selector()] 得到的是 Foundation 框架下的 (Foundation_NSSetObjectValueAndNotify)` 。