前言
KVO 也适用于传值,在之前的学习只是学习了KVO的传值,今天详细学习 监听和实现
源码放在下一节学习
1.1 KVO
KVO(Key-Value Observing
)是Objective-C语言中一种观察者模式的实现,可以用来监听对象属性值的变化。KVO机制允许一个对象注册为另一个对象的属性变化的观察者,并在被观察的属性值发生变化时,自动接收通知并进行相应处理。
KVO可以实现监听某个属性的变化 KVO机制只能监听对象属性值的变化,无法监听基本数据类型的变化,需要将其封装为对象属性才能实现传递。)
KVO可以实现界面之间的传值,跨界面可以。
1.2 使用KVO
现在有如下的场景
我们点击change按钮需要改变 上面的Label,同时我们需要监听Label的变化,看看如何用KVO实现键值监听
初始化按钮
- (UILabel *)test_label_init {
if (!self.test_label) {
self.test_label = [[UILabel alloc] init];
self.test_label.text = @"Label not Change";
self.test_label.font = [UIFont systemFontOfSize:25];
[self.view addSubview:self.test_label];
self.test_label.frame = CGRectMake(120, 170, 300, 30);
[self.test_label addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
return self.test_label;
}
- (UIButton *)test_button_init {
if (!self.test_button) {
self.test_button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[self.test_button setTitle:@"change" forState:UIControlStateNormal];
[self.test_button addTarget:self action:@selector(changeLabel) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.test_button];
self.test_button.frame = CGRectMake(120, 260, 100, 100);
self.test_button.backgroundColor = [UIColor redColor];
}
return self.test_button;
}
注册KVO监听
通过[addObserver:forKeyPath:options:context:]
方法注册KVO,这样可以接收到keyPath属性的变化事件;
observer
:观察者,监听属性变化的对象。该对象必须实现observeValueForKeyPath:ofObject:change:context: 方法。
keyPath
:要观察的属性名称。要和属性声明的名称一致。
options
:回调方法中收到被观察者的属性的旧值或新值等,对KVO机制进行配置,修改KVO通知的时机以及通知的内容
context
:传入任意类型的对象,在"接收消息回调"的代码中可以接收到这个对象,是KVO中的一种传值方式。、
KVO监听实现
通过方法[observeValueForKeyPath:ofObject:change:context:]
实现KVO的监听;
keyPath
:被观察对象的属性
object
:被观察的对象
change
:字典,存放相关的值,根据options传入的枚举来返回新值旧值
context
:注册观察者的时候,context传递过来的值
点击Button
移除KVO监听
在不需要监听的时候,通过方法[removeObserver:forKeyPath:]
,移除监听;
禁止KVO
我们可以手动禁止KVO监听某个属性的变化
automaticallyNotifiesObserversForKey
:是一个可选的类方法,用于自定义KVO机制中属性变化通知的行为。当对象的属性发生变化时,系统会自动调用这个方法来获取是否自动发送通知。
这个方法通常被用于实现一些高级的KVO机制,比如当某个属性的变化依赖于其他属性时,可以在这个方法中检测相关属性的变化情况,从而决定是否发送属性变化通知。
我们实现一个Label的分类,在里面重写这个方法
#import "UILabel+autoCall.h"
@implementation UILabel (autoCall)
// 选择性的实现KVO
// 因为是类方法 并且改变的是Label 所以需要创建一个分类来给UILabel重写这个方法 即可完成禁止通知
// 那么也就不会实现 observeValueForKeyPath
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"text"]) {
NSLog(@"NO WAY");
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
// return NO;
}
@end
1.3 KVO实现监听数组内部元素变化
KVO默认只能监听到数组对象本身的变化,而无法监听到数组内部元素的变化。例如,如果将一个对象添加到数组中,KVO会收到数组count属性的变化通知,但不会收到数组内部元素的变化通知。
KVO机制只能监听对象属性值的变化,无法监听基本数据类型的变化,那么如何实现KVO监听数组内部元素的变化?
KVO(Key-Value Observing
)是Objective-C语言中一种观察者模式的实现,可以用来监听对象属性值的变化。但是KVO不能直接监听数组的变化,因为NSArray和NSMutableArray并没有实现KVO机制。如果需要监听数组的变化,可以使用以下两种方式:
1.3.1 手动触发KVO通知
当数组中的元素发生变化时,手动触发KVO通知即可实现监听。具体实现方式如下:
在被观察对象的类中,重写该对象所包含的可变数组的对应方法,比如addObject:、removeObject:、insertObject:atIndex:等方法。
在重写的方法中,调用willChangeValueForKey:和didChangeValueForKey:方法,手动触发KVO通知。
- (void)addObject:(id)anObject {
[self willChangeValueForKey:@"myArray"];
[super addObject:anObject];
[self didChangeValueForKey:@"myArray"];
}
在观察者中注册被观察对象的数组属性,当数组中的元素发生变化时,观察者会收到KVO通知。
[observedObject addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
需要注意的是,手动触发KVO通知需要在重写的方法中手动添加代码,实现起来比较麻烦,容易出错,因此不是一种推荐的方式。
1.3.2 使用NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
选项
KVO支持使用NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
选项**,来监听可变数组中的元素变化。这两个选项会在KVO通知中包含新旧值的信息,因此可以在观察者中获取到数组中元素的变化。**
[observedObject addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,根据KVO通知中的信息来处理数组元素的变化。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"myArray"]) {
NSArray *oldArray = change[NSKeyValueChangeOldKey];
NSArray *newArray = change[NSKeyValueChangeNewKey];
// 处理数组元素的变化
}
}
这种方式需要被观察的对象的数组属性必须是可变的,而且只能监听到元素的增加、删除和替换操作,
1.3.4 使用KVO的注意事项⚠️
- keyPath 不能为空字符串
- 注意在适合的地方removeObersver,如果观察实例比被观察实例先释放,这时候改变观察属性,会产生崩溃。
- 没有添加,直接移除观察关系,也会产生崩溃
1.4 KVO的实现
KVO的实现原理分为3步走
1.4.1 实现
首先明确 KVO机制的实现原理是,当一个对象被观察时,系统会动态地生成一个派生类,并将被观察对象的isa指针指向该派生类。这个派生类重写了被观察对象的setter方法,在setter方法中,除了进行属性值的赋值操作,还会通知观察者对象属性值的变化。
如何查看派生类?
情景:现在有一个testClass的类,里面有一个className属性,现在监听className属性
self.testClass = [[TestClass alloc] init];
NSLog(@"%s: isa = %s", object_getClassName(self.testClass), class_getName(object_getClass(self.testClass)));
[self.testClass addObserver:self forKeyPath:@"className" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
NSLog(@"%s: isa = %s", object_getClassName(self.testClass), class_getName(object_getClass(self.testClass)));
其中,object_getClassName()函数可以返回对象所属的类名,object_getClass()函数可以返回对象的实际类(class),而不是对象所属的类的类型。
当这段代码执行后,就会在控制台输出该对象的类名和实际类名。实际类名即为系统动态生成的派生类名,以NSKVONotifying_
为前缀,后面紧跟着被观察对象的类名。例如,如果被观察对象是一个Person对象,那么实际类名就是NSKVONotifying_Person。
注意,这种方法只适用于Objective-C的KVO机制。对于Swift的KVO机制,由于其机制不同,不能通过此方法来打印查看。
如何生成派生中间类?
isa-swizzling(类指针交换):
就是把当前某个实例对象的isa指针指向一个新建造的中间类,在这个新建造的中间类上面做hook方法或者别的事情,这样不会影响这个类的其他实例对象,仅仅影响当前的实例对象。
在添加观察者之后 NSKVONotifying_testClass如何内部实现
1.4.2 NSKVONotifying_testClass如何内部实现
- setName:最主要的重写方法,set值时调用通知函数
- class:返回原来类的class
- dealloc
- _isKVOA判断这个类有没有被KVO动态生成子类
- (void)setClassName:(NSString *)className {
}
- (Class)class {
- 这是为了保证该中间类在外部使用时可以替代原始类,实现完全透明的KVO功能。
return [testClass class];
}
- (void)dealloc {
// 收尾工作
}
- (BOOL)_isKVOA {
- 添加一个名为_isKVOA的实例变量**,用于标识该对象是否支持KVO机制。
return YES;
}
isa指向中间类之后如何调用方法:
对于这两个属性 我们只监听了className属性而没有监听testArray属性
- 调用监听的属性的设置方法,例如:
setClassName
:,都会先调用NSKVONotify_testClass
对应的属性设置方法 - 调用非监听属性的设置方法,如
setClassArray
方法,就会通过NSKVONotify_Apple
的superClass
来找到testClass
类对象,在调用其Apple类对象中的test方法
重写Class方法 :这是为了保证该中间类在外部使用时可以替代原始类,实现完全透明的KVO功能。
添加一个名为_isKVOA的实例变量,用于标识该对象是否支持KVO机制。
1.4.3 _NSSetObjectValueAndNotify
在具体实现过程中,系统会动态生成一个继承自原始类的中间类,并且在该类的初始化方法中,调用了一个叫做_NSSetObjectValueAndNotify
()的函数,用于实现属性改变的通知。
_NSSetObjectValueAndNotify()函数的实现过程如下:
a) 首先会调用 willChangeValueForKey
b) 然后给属性赋值
c) 最后调用 didChangeValueForKey
d) 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .
1.5 总结KVO
1.5.1 KVO的本质
- 利用runtime的API动态生成一个子类,并让实例对象的isa指向这个全新的子类
- 当修改实例变量对象的属性时候,在全新子类的set方法中会调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey - 调用原来的
setter
didChangeValueForKey
:内部会触发监听器的监听方法
1.5.2 KVO使用场景
- 对于时刻变化的对象,例如colletionView的items,总是动态的变化,这个时候可以使用KVO监听对象。
- 在AVFounditon中获取AVPlayer的播放进度,播放状态,也需要使用KVO来观察。
1.5.3 实现过程总结
- 在
addObserver:forKeyPath:options:context:context
调用的时候,会自动生成并注册一个该对象(被观察的对象)对应类的子类,取名NSKVONotify_Class,并且将该对象的isa指针指向这个新的类。 - 在该子类内部实现4个方法-被观察属性的set方法、class方法、isKVO、delloc。
- 最关键的是set方法中,先调用
willChangeValueForKey
,再给成员变量赋值,最后调用didChangeValueForKey
,willChangeValueForKey和didChangeValueForKey需要成对出现才能生效,在didChangeValueForKey中会去调用观察者的observeValueForKeyPath: ofObject: 方法。 - 重写class方法,这样避免外部感知子类的存在,同时防止在一些使用isKindOfClass判断的时候出错。
- isKVO方法作为能否实现KVO功能的一个标识。
- delloc里面还原isa指针