KVO 的基本原理
KVO 是 key/value/observer 的缩写。
表示的意思是:当某个属性的值发生变化的时候,通知观察者。
在直白一点,当某个对象的属性调用 setter 方法的时候,通知观察者。
这里面有个 观察者,所以,KVO 的本质上就是一个观察者模式。
观察者模式
定义:
事件的发布者发布事件并执行事件,订阅者订阅事件并提供事件响应函数。
一个事件机制包含:
- 事件发布者。
- 事件订阅者。
- 事件发布者发布并执行事件。
- 事件订阅者订阅并提供事件响应函数。
对应到 iOS 中的 KVO。
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
}
self.person 事件发布者 -> self.person
当前 Person 对象
addObserver:self 事件订阅者 -> self
当前控制器
self.person setName: --> 事件发布者发布并执行事件。
**- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> )change context:(void )context --> 事件订阅者订阅并提供事件响应函数。
针对于 iOS 的 KVO 来说,可以这么简单的理解。
当期控制器作为观察者订阅
Person
的某个属性的setXXXX:
方法。
当这个setXXXX:
方法执行过程中,会调执行事件,执行控制器提供的事件响应方法。
KVO 的基本使用
当前控制器订阅某个对象的某个属性的改变
注册事件订阅者,观察对象的属性。
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
事件订阅者提供事件响应函数。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@",change);
}
注意:KVO 使用注意移除事件观察者。
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}
KVO的调试
代码执行到 28 行。
当前 self.person
的类型
执行到 29 行之后。
当前 self.person
的类型。
如果看不到这个类型,可以 cmd + q 退出 xcode 在重新打开一次。
发现当前 self.person
的 isa 指针指向了 NSKVONotifying_RLPerson
类型。
NSKVONotifying_RLPerson
在执行了 [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
这行代码之后,self.person 的 isa 指针变成了 NSKVONotifying_RLPerson
类型了。
目前为止,不清楚,为什么会有这么一个类型。
但可以通过 runtime 的方式,来获取这个类型的一些属性和方法看看。
查看 NSKVONotifying_RLPerson 类的属性。
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
// 当29行代码执行完毕,self.person 的 isa 指针指向了 NSKVONotifying_RLPerson 类型。
Class newClass = NSClassFromString(@"NSKVONotifying_RLPerson");
unsigned int count;
objc_property_t *ps = class_copyPropertyList(newClass, &count);
NSLog(@"-----NSKVONotifying_RLPerson 属性类表 BEGIN----");
for (NSInteger i = 0; i < count; i++) {
NSLog(@"%s",property_getName(ps[i]));
}
NSLog(@"-----NSKVONotifying_RLPerson 属性类表 END----");
运行结果:
2018-03-25 01:11:17.008 KVO-CC[33470:16404323] -----NSKVONotifying_RLPerson 属性类表 BEGIN----
2018-03-25 01:11:17.008 KVO-CC[33470:16404323] -----NSKVONotifying_RLPerson 属性类表 END----
发现,此类好像没有属性?
查看 NSKVONotifying_RLPerson 类的方法。
Method *ms = class_copyMethodList(newClass, &count);
NSLog(@"-----NSKVONotifying_RLPerson 方法列表 BEGIN----");
for (NSInteger i = 0; i < count; i++) {
NSLog(@"%@", NSStringFromSelector(method_getName(ms[i])));
}
NSLog(@"-----NSKVONotifying_RLPerson 方法列表 END----");
运行结果:
2018-03-25 01:19:04.595 KVO-CC[33532:16417767] -----NSKVONotifying_RLPerson 方法列表 BEGIN----
2018-03-25 01:19:04.595 KVO-CC[33532:16417767] setName:
2018-03-25 01:19:04.595 KVO-CC[33532:16417767] class
2018-03-25 01:19:04.596 KVO-CC[33532:16417767] dealloc
2018-03-25 01:19:04.596 KVO-CC[33532:16417767] _isKVOA
2018-03-25 01:19:04.596 KVO-CC[33532:16417767] -----NSKVONotifying_RLPerson 方法列表 END----
发现 NSKVONotifying_RLPerson 中有 4个方法。
- setName:
- class
- dealloc
- _isKVO
反正就是由4个方法。其中一个还是 setName:
在我们添加 KVO 的时候,被观察的对象 RLPerson
里有就这么一个方法。
理性感觉上,NSKVONotifying_RLPerson
应该是这个类的子类,否则,为什么这么巧合,它也有一个 setName:
的方法?
查询 KSNVONotifying_RLPerson的父类是否是 RLPerson
在此之前,要先了解一张图。补充一下知识点。
图片的最左边,就是我们经常用到的 instance 实例对象。
实例对象的虚线(isa)直接指向了是当前实例的类型,也就是类(对象)
在类对象的右边,仍然有一条(isa)指针,它指向的是 metaClass。 也就是基元类。
前半段很好理解。
RLPerson *p = [RLPerson new];
p 是实例,RLPerson 是 P 实例的类型。
但是对后半段的解读则是:在某种程度上,RLPerson 也是对象,它是由它的基元类实例化出来的对象。
类对象和基元类对象的异同?
- 类对象一般会存储实例对象的一些 Method。也就是实例方法。
- 基元对象,是类对象的模板,它可以实例化出来类对象(不是实例对象)。类对象的类方法,一般是存储在它的基元类当中的。
上面只是补充知识点,以免自己遗忘。
4个方法中的 setName:
因为 NSKVONotifying_RLPerson 里面有个 setName: 而 RLPerson 里面也有个 setName: 这应该不是巧合,它俩之间应该存储某种关系?而且应该是父子类关系。
// 获得NSKVONotifying_RLPerson 的父类
Class superClass = class_getSuperclass(newClass);
NSString *superClassName = NSStringFromClass(superClass);
NSLog(@"NSKVONotifying_RLPerson 的 父类是 : %@",superClassName);
运行结果:
2018-03-25 11:06:12.138 KVO-CC[33712:16460834] NSKVONotifying_RLPerson 的 父类是 : RLPerson
确定了,NSKVONotifying_RLPerson
是 RLPerson
的子类,也就确定了RLKVONotifying_RLPerson
的 setName:
是来自 RLPerson
的 setName:
继承。
那这个 setName :
是单纯的继承过来,还是自己又做了什么额外的处理。
每个赋值属性本质上 setter
,setter 里面可能有负责的逻辑,可能也只是一个简单的赋值。
但在添加了 KVO 之后,原来对象的 setter 逻辑不变,会在此逻辑的上下各加一行代码,完成 KVO 的触发。
RLKVONotifying_RLPerson 重写的 setName:逻辑
[self willChangeValueForKey:@"name"];
[super setName:xxxxxx]; // RLPerson 自己的处理 setter 逻辑
[self didChangeValueForKey:@"name"];
证明 KVO 的执行发布通知的核心,就是这上下两行代码。
首先把 person 对象的 age 观察添加上。
[self.person addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew) context:nil];
但是,不去触发这个属性的 KVO。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return YES;
}
运行结果:
2018-03-25 12:01:25.799 KVO-CC[34213:16541280] {
kind=1,
new=李四**,
}
2018-03-25 12:01:26.021 KVO-CC[34213:16541280] {
kind=1,
new=李四***,
}
KVO 只对 name
属性生效。
实现 age 属性的 setAge:方法,并加上上述两行 KVO 的核心代码。
- (void)setAge:(NSUInteger)age {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
运行结果:
所以,KVO 的执行确实是由 [self willChangeValueForKey:@"XXX"];
&& [self didChangeValueForKey:@"XXX"];
两行代码生效的。
上面这段,也就是常说的 手动触发 KVO。
总结一下手动调用 KVO 基本步骤:
- 仍然需要把属性注册到 KVO 机制里面。
- 在当前对象中重写
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
告知,那些属性不需要自动触发 KVO。 - 在属性的 setXXX: 方法里,手动调用
[self willChangeValueForKey:@"XXX"];
&&[self didChangeValueForKey:@"XXX"];
4个方法中的class
在 RLPerson 对象添加 KVO 观察之后,类型已经变成了 NSKVONotifying_RLPerson 了。
那么,在 NSKVONotifying_RLPerson 里的 class 方法是啥意思呢?
按道理来说,既然 RLPerson 对象的 isa 指指向了 NSNotifying_RLPerson 它返回的 class 类型应该也是 NSKVONotifying_RLPerson 才对。
// RLPerson 在添加 KVO 之后,类型变成了 NSKVONotifying_RLPerson 了。但此新类型中,有一个 class 的方法。
NSString *className = NSStringFromClass([self.person class]);
NSLog(@"className : %@",className);
而实际情况是:
className : RLPerson
仍然是原来的 RLPerson 类型。
所以在 NSNotifying_RLPerson 这个类中重写 class 方法,本质就是返回当前对象原本的类型。苹果想隐藏 KVO 的内部实现细节。
4个方法中的 dealloc
因为,毕竟创建了一个新的类型,在取消观察者的时候,会把这个新的类型删除。所以,实际就在这个 dealloc 方法里。
4个方法中的 _isKVO
看文档和博客,说的是,标记这个新类 KVO 机制新建的。
基本总结
KVO 的实现基本原理:
- 创建当前类的子类,类型为 NSNotifying_类名。
- 在此类中,重写了4个方法
setXxxx:
class
,dealloc
,_isKVO
。 -
setXXXX:
内部保留了原来的 setXXX 逻辑之外,在上下都加了两行self willChangeValueForKey:@"XXX"];
&&[self didChangeValueForKey:@"XXX"];
- 其他3个方法,
class
隐藏KVO 内部实现细节、dealloc
释放 KVO 产生的新类资源、_isKVO
标记此类的作用。