前言
KVO相信大家都用得不少,但是不一定所有的细节我们都能清楚,今天我们就一起学习总结一下,附上KVO官方文档链接
一.关于KVO的一些细节分享
1.context作用
首先我们看一下context的相关说明
大致意思是:addObserver:forKeyPath:options:context:消息中的上下文指针包含任意数据,这些数据将在相应的更改通知中传回给观察者。您可以指定NULL并完全依赖键路径字符串来确定更改通知的来源,但是这种方法可能会导致超类由于不同原因也在观察相同键路径的对象出现问题。
一种更安全、更可扩展的方法是使用上下文来确保您收到的通知是发送给您的观察者而不是超类的。
类中唯一命名的静态变量的地址是一个很好的上下文。在超类或子类中以类似方式选择的上下文不太可能重叠。您可以为整个类选择一个上下文,并依靠通知消息中的关键路径字符串来确定发生了什么变化。或者,您可以为每个观察到的键路径创建一个不同的上下文,这完全绕过了字符串比较的需要,从而提高了通知解析的效率。清单 1显示了以这种方式选择的balance和interestRate属性的示例上下文。
示例代码如下
self.person = [LGPerson new];
self.student = [LGStudent new];
//假如对多个熟悉进行监听
//多对象键值观察
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:@"personNick"];
[self.student addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:@"studentNick"];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
// 性能 + 代码可读性
//context 相当于标识符 判断
if (context == @"personNick") {
NSLog(@"%@",change);
} else if (context == @"studentNick") {
NSLog(@"%@",change);
}
NSLog(@"%@",change);
}
使用场景:如果有多个属性要进行监听,他们是多层嵌套关系,比如是父类的父类的熟悉,又或者是不同对象属性重名等等情况,我们在回调方法做判断的话,代码的逻辑就会显得很繁琐,使用context做标志,就可以直接区分,代码可读性更强,性能也有所提升
2.观察者是否移除
对于移除观察者,我相信大家都知道,一般都会在dealloc方法中进行移除,但是为什么要移除呢,我们看下官方文档的相关部分的解释
- An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
- The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.
是当调用dealloc时,观察者不会自动删除自己,当被观察者继续发送通知的时候,可能会给已经释放掉的观察者发送消息,最终造成了内存的访问异常。因此,需要保证当观察者被销毁时,将观察者移除。
该协议没有提供询问对象是观察者还是被观察者的方法。一个典型的模式是在观察者初始化期间注册为观察者(例如 在init或viewDidLoad)并在释放期间取消注册(通常是在dealloc),确保正确配对和有序添加和删除消息,并且观察者在从内存中释放之前被取消注册.
所以说在观察者被释放之前,就需要取消注册,因此在dealloc中取消注册时最合适的
3.KVO手动监听和自动监听
当然默认情况下都是自动监听,但是可能也会存在某些场景需要手动监听,比如设置一些开关,在某些固定情况下,去观察部分属性,其他时候又要观察全部属性,这时候就可以使用2种写法,根据开关切换
//手动
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
//可以根据不同的key值,来区分使用自动还是手动监听
//if(@"开关"){
return NO;
}
return YES;
}
4.观察受多个因素影响的属性
什么叫受多个因素影响的属性,举个常见的例子
下载:一般下载我们需要知道的是下载进度,而下载进度 = 已下载 / 总数。如果已下载和总数都是在不断变化的,那么我们该怎么做才能对下载进度进行观察呢?请看下面的例子
监听downloadProgress,通过点击更新下载和总数
self.person = [LGPerson new];
[self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.writtenData += 10;
self.person.totalData += 1;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
关联属性,通过判断key值,只要totalData,writtenData发生变化就会监听downloadProgress的变化,然后在重写getter方法,写出三者运算关系即可
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (NSString *)downloadProgress{
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
通过点击打印结果:因为点击一次totalData,writtenData都改变了,所以打印downloadProgress2次
2021-07-29 12:24:29.374538+0800 001---KVO初探[1343:92197] {
kind = 1;
new = "0.198020";
}
2021-07-29 12:24:29.374701+0800 001---KVO初探[1343:92197] {
kind = 1;
new = "0.196078";
}
5.对可变数组的观察
对于使用KVO去监听集合类型数据的变化,那就依赖于对KVC的理解,当然官方文档也有解释,我这里就不一一解释了,大家可以去KVO官方文档链接看,代码如下
self.person = [DMPerson new];
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"xinge"];
打印结果
2021-07-29 12:32:14.290173+0800 001---KVO初探[1384:98856] {
indexes = "<_NSCachedIndexSet: 0x6000009d55c0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
xinge
);
}
二.KVO原理分析
其实KVO的原理大家可能或多或少都知道一些,因为这些在面试中也会经常遇到,但是有没有真正去求证呢?今天我们就一起去求证一下
1.动态生成派生类-NSKVONotifying_xxx
我们通过LLDB调试验证,在添加观察者之前打断点
走到下一步之后在打印,确实动态生成了NSKVONotifying_LGPerson
当然呢,为了以防万一,我也可以看看是不是在编译的时候就生成了,我们可以在添加观者者之前看NSKVONotifying_LGPerson是否存在即可
通过这个方法可以遍历当前类和所有的子类
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
在前后分别打印即可
[self printClasses:[LGPerson class]];
self.person = [[LGPerson alloc] init];
[self printClasses:[LGPerson class]];
打印结果如下
2021-07-29 12:52:02.222312+0800 002---KVO原理探讨[1590:113159] classes = (
LGPerson,
LGStudent
)
2021-07-29 12:52:02.224991+0800 002---KVO原理探讨[1590:113159] classes = (
LGPerson,
"NSKVONotifying_LGPerson",
LGStudent
)
由此可以看出,确实是在添加观察者时动态生成了NSKVONotifying_LGPerson,并且是子类,那么我们接下来看下这个子类存放哪些东西,根据之前对于类的结构分析,都知道类存放的无非就是成员变量,方法,协议等一些信息,那么我们可以通过下面的方法进行遍历类的方法
#pragma mark **- 遍历方法-ivar-property**
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
分别打印NSKVONotifying_LGPerson和LGPerson,结果如下
2021-07-29 19:25:56.595778+0800 002---KVO原理探讨[1058:62119] 获取派生类方法
2021-07-29 19:25:56.596011+0800 002---KVO原理探讨[1058:62119] setNickName:-0x10c7b503f
2021-07-29 19:25:56.596185+0800 002---KVO原理探讨[1058:62119] class-0x10c7b3b49
2021-07-29 19:25:56.596282+0800 002---KVO原理探讨[1058:62119] dealloc-0x10c7b38f7
2021-07-29 19:25:56.596380+0800 002---KVO原理探讨[1058:62119] _isKVOA-0x10c7b38ef
2021-07-29 19:25:56.596499+0800 002---KVO原理探讨[1058:62119] 获取当前类方法
2021-07-29 19:25:56.596609+0800 002---KVO原理探讨[1058:62119] setNickName:-0x10c4a44c0
可以发现,子类其实是重写了父类的这些方法,至于_isKVOA则是用于判断这个类是不是动态生成的派生类
2.派生类是否会在移除观察者后销毁
首先看下dealloc,之前我们也说过,当我们不需要观察者时,需要移除观察者,否则可能会造成内存访问异常。同时也知道添加观察者时,其实就是改变了isa的指向,指向了新生成的派生类,那么当我们移除观察者时,来看下isa是不是指回原来的类
可以发现在dealloc中移除观察者之后,isa确实指回原来的类,那么这个派生类NSKVONotifying_LGPerson会不会就此销毁呢?我们来进一步验证,验证也很简单,在dealloc之后,我们在LLDB打印NSKVONotifying_LGPerson看是否存在即可
我们在离开当前类,再进入重新添加观察者之前就打印派生类NSKVONotifying_LGPerson,发现确实存在,那么就说明这个派生类已经有缓存了,下次添加观察者时,就不用重新创建了,那也就说明了,其实在这里也调用了NSKVONotifying_LGPerson重写的dealloc方法,在重写的dealloc方法中,实现了isa的重新指向
3.分析派生类重写的方法
既然重新创建了一个新的子类,那么我们就来看看这个类有什么不同之处,看下重写的类方法做了什么,dealloc已经说了,接下来看下setter方法,首先给LGPerson添加一个成员变量name
为什么这么做呢,因为只有属性才会生成setter,getter方法,成员变量没有,那么接下来给他们都添加观察者进行监听
可以发现,成员变量的值发生改变并没有监听到,这是不是就证明了一点,其实所谓的观察其实就是监听setter方法的调用,这是不是也说明了上面提到KVO手动监听时,为什么会重写setter方法了,接下来我们在dealloc移除观察者之后,做些测试
可以发现,移除观察者后之后,按理说isa已经重新指回LGPerson,而前面调用的setter方法应该是调用的派生类NSKVONotifying_LGPerson的setter方法,为什么现在LGPerson的属性也发生改变了呢,是不是也就说明了,派生类NSKVONotifying_LGPerson的setter方法,其实也给父类的属性赋值了,当然还是要验证一下,如何验证呢?我们只需要在调用setter方法时获取堆栈信息即可
通过下符号断点获取setter方法区间的调用栈,通过触发调用,直接进入汇编,这样就可以获取底层的堆栈信息了
通过堆栈信息可以发现在调用touchesBegan:withEvent:之后通过一系列调用又重新执行了LGPerson的setter方法,由于#2-#4的步骤都是Foundation库的方法,并不开源的,当然呢class方法也会有类似的操作,我们也可以简单试验一下
同样的,在添加观察者前后分别调用class方法,打印结果确一样,说明派生类NSKVONotifying_LGPerson在调用class方法的过程也进行了一系列的操作,最终又调回了父类的class方法,从而达到隐藏中间的执行过程,并且行成闭环,这个我就不在下符号断点调试了,今天的探索暂时就到次为止
4.KVO流程相关总结
-
1.添加观察者后,会自动生成一个派生类,命名为NSKVONotifying_xxx,同时将实例对象的isa指向派生类,派生类是被观察对象的子类
-
2.NSKVONotifying_xxx重写了父类的class,dealloc,setter方法,目的为了隐藏底层方法的真正执行流程,行成闭环,并且生成了一个_isKOVA的标识方法,用于判断是不是派生类
-
3.对对象属性的观察其实就是监听对象属性的setter方法的调用(完美契合为什么手动监听会重写setter方法)
-
4.移除观察者并不是真正的移除,只是把isa重新指会原对象,并且保存了派生类,以免下次添加观察者时又要重新创建
今天的分享就到次为止,下一篇章自定义KVO