KVO
KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。
观察者模式是什么
一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。
简单来说KVO可以通过监听key,来获得value的变化,用来在对象之间监听状态变化。KVO的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueObserving类别名,所以对于所有继承了NSObject的类型,都能使用KVO(一些纯Swift类和结构体是不支持KVO的,因为没有继承NSObject)。
KVO底层探索
#import "ViewController.h"
#import "MJPerson.h"
@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.person1 setAge:21];
[self.person2 setAge:22];
}
- (void)dealloc {
// 不要忘记移除监听,否则会导致内存泄露
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end
复制代码
结果输出:
监听到<MJPerson: 0x6000039843f0>的age属性值改变了 - {
kind = 1;
new = 21;
old = 1;
} - 123
复制代码
那么问题来了,同样是调用 MJPerson 的 setAge 方法,为什么 person1 就会发送属性值改变的通知,而 person2 就不会呢?在给 person1 添加KVO监听的时候,到底都发生了什么呢?
我们都知道,实例对象在调用对象方法的时候,首先通过 isa 指针找到自己的类对象,然后在类对象的对象方法列表中查找,找不到则通过 supperClass 去父类中查找,那么既然执行的是同一个方法,结果却并不相同,那么我们可以猜测,person1 和 person2 的 isa 指针指向的肯定是两个不同的类,我们通过 LLDB 指令来验证一下:
(lldb) p _person1.isa
(Class) $1 = NSKVONotifying_MJPerson
Fix-it applied, fixed expression was:
_person1->isa
(lldb) p _person2.isa
(Class) $2 = MJPerson
Fix-it applied, fixed expression was:
_person2->isa
复制代码
可以看到 person1 的 isa 指针指向的是 NSKVONotifying_MJPerson,而 person2 的 isa 指针指向的是 MJPerson,下面我们在原来代码的基础上,稍作修改:
NSLog(@"person添加KVO之前 - person1:%@, person2:%@",object_getClass(self.person1), object_getClass(self.person2));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person添加KVO之前 - person1:%@, person2:%@",object_getClass(self.person1), object_getClass(self.person2));
复制代码
结果输出:
person添加KVO之前 - person1:MJPerson, person2:MJPerson
person添加KVO之后 - person1:NSKVONotifying_MJPerson, person2:MJPerson
复制代码
由此可见在没有为 person1 添加KVO之前 person1 的 isa 指针仍然是指向 MJPerson,而添加KVO监听之后,就改变为指向 NSKVONotifying_MJPerson。
我们添加一下代码,看看 NSKVONotifying_MJPerson 是从哪来的:
Class person1Before = object_getClass(self.person1);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
Class person1After = object_getClass(self.person1);
复制代码
输入 LLDB 指令:
(lldb) p person1Before.superclass
(Class) $0 = NSObject
(lldb) p person1After.superclass
(Class) $1 = MJPerson
复制代码
可以看到,NSKVONotifying_MJPerson 的 superclass 是 MJPerson,那么可以推测出来,NSKVONotifying_MJPerson 这个类是在对 person1 添加了KVO监听之后,由 Runtime 动态生成出来的 MJPerson 的子类,并且把 person1 的 isa 指针指向这个子类。而在调用 setAge 这个方法时会产生不同的效果,肯定是 NSKVONotifying_MJPerson 重写了 setAge 方法,在其中加入了KVO监听相关的代码,我们再来验证一下:
NSLog(@"person添加KVO之前 - person1:%p, person2:%p \n",[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person添加KVO之后 - person1:%p, person2:%p \n",[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
复制代码
结果输出:
person1添加KVO监听之前 - 0x101f29650 0x101f29650
person1添加KVO监听之后 - 0x7fff257223da 0x101f29650
复制代码
- (IMP)methodForSelector:(SEL)aSelector
是用来获取方法实现的具体地址,由此可见,在添加KVO监听之前 person1 和 person2 的 setAge 方法的实现是同一个,添加之后 person1 的 setAge 方法的实现就有了变化。
接下来我们再用 LLDB 指令来看看方法实现的名称
(lldb) p (IMP)0x101f29650
(IMP) $0 = 0x0000000101f29650 (Interview01`-[MJPerson setAge:] at MJPerson.m:12)
(lldb) p (IMP)0x7fff257223da
(IMP) $1 = 0x00007fff257223da (Foundation`_NSSetIntValueAndNotify)
复制代码
p (IMP)方法地址
可以用来打印方法的名称,由此可见,在添加KVO监听之后 person1 的 setAge:
方法调用了 Foundation 框架中的 _NSSetIntValueAndNotify()
方法实现。
如果定义的属性是类型是double则调用的是 _NSSetDoubleValueAndNotify()
大家可以自己测试一下。 此方法在Foundtion框架中有对应的
NSSetDoubleValueAndNotify()
NSSetCharValueAndNotify()
…
具体 _NSSetIntValueAndNotify()
方法是如何实现的,这个涉及到对 Foundation 框架的逆向,不是本节的重点,有兴趣的朋友可以自行查找一下相关的内容,我在这里只讲一下结论:
// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
复制代码
那么关于这个结论,怎么验证一下呢?
@implementation MJPerson
- (void)setAge:(int)age{
_age = age;
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
复制代码
结果输出:
willChangeValueForKey
didChangeValueForKey - begin
监听到<MJPerson: 0x6000007e45c0>的age属性值改变了 - {
kind = 1;
new = 21;
old = 1;
} - 123
didChangeValueForKey - end
复制代码
所以可以大致说明 _NSSetIntValueAndNotify 的内部实现:
- 调用 willChangeValueForKey:
- 调用原来的 setter 实现
- 调用 didChangeValueForKey:
didChangeValueForKey:
内部会调用observer的observeValueForKeyPath:ofObject:change:context:
方法
NSKVONotifying_MJPerson重写方法
重写setter
在 setter 中,会添加以下两个方法的调用。
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
复制代码
然后在 didChangeValueForKey:
中,去调用:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context;
复制代码
因为 KVO 的原理是修改 setter 方法,因此使用 KVO 必须调用 setter ,若直接访问属性对象则没有效果。
重写class
- (Class)class
{
return [MJPerson class];
}
复制代码
屏蔽内部实现,隐藏了 NSKVONotifying_MJPerson 类的存在
重写dealloc
系统重写 dealloc 方法来释放资源。
重写_isKVOA
这个私有方法是用来标示该类是一个 KVO 机制生成的类。
面试题
1.KVO的本质是什么?
KVO 是通过 isa-swizzling 实现的。
KVO 的本质其实就是,编译器自动为被观察对象创建了一个派生类,并将被观察对象的isa 指向这个派生类。派生类是被观察对象的原始类别的子类,如果用户注册了对某此目标对象的某一个属性的观察,那么此派生类会重写该属性的 setter 方法,并在其中添加进行通知的代码。
2.如何手动触发KVO?
KVO的实现,是对注册的keyPath中自动实现了两个函数,在setter中,自动调用。
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
复制代码
所以如果想手动进行KVO,首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey:
和 didChangeValueForKey:
方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;其次,要实现类方法 automaticallyNotifiesObserversForKey
,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
- (void) setAge:(int)theAge
{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
复制代码
如果需要禁用该类KVO的话直接 automaticallyNotifiesObserversForKey
返回NO,实现属性的 setter 方法,不进行调用 willChangeValueForKey:
和 didChangeValueForKey:
方法。
KVC
KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的。
常见的API:
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
复制代码
- keyPath 相当于根据路径去寻找属性,一层一层往下找,
- key 是直接拿取属性的名字设置,如果按路径找会报错
setValue:forKey:原理
- 首先按照 setKey、_setKey 的顺序来查找是否有对应的 setter 方法,有则调用,否则走下一步
- 没有找到对应的 setter 方法,接下来会查看
+ (BOOL)accessInstanceVariablesDirectly
方法有没有返回 YES,默认该方法会返回 YES。- 如果你重写了该方法让其返回 NO 的话,那么在这一步KVC会调用
setValue:forUndefinedKey:
方法,并抛出异常 NSUnknownKeyException。 - 返回YES,接下来KVC会按照
_key
,_iskey
,key
,iskey
的顺序搜索成员变量,找到了就进行赋值操作,若所有成员变量都不存在,KVC会调用setValue:forUndefinedKey:
方法,并抛出异常 NSUnknownKeyException。
- 如果你重写了该方法让其返回 NO 的话,那么在这一步KVC会调用
valueForKey:原理
- 首先按照 getKey、key、isKey、_key 的顺序来查找是否有对应的 getter 方法,有则调用,否则走下一步
- 没有找到对应的 getter 方法,接下来会查看
+ (BOOL)accessInstanceVariablesDirectly
方法有没有返回 YES,默认该方法会返回 YES。- 返回 NO,那么这一步KVC会调用
valueForUndefinedKey:
方法并抛出异常 NSUnknownKeyException。 - 返回YES,接下来KVC会按照
_key
,_iskey
,key
,iskey
的顺序搜索成员变量,找到了就进行取值操作,若所有成员变量都不存在,KVC会调用valueForUndefinedKey:
方法并抛出异常 NSUnknownKeyException。
- 返回 NO,那么这一步KVC会调用