ToB企服应用市场:ToB评测及商务社交产业平台

标题: 【iOS】KVO [打印本页]

作者: 王柳    时间: 2024-6-21 13:11
标题: 【iOS】KVO

前言

KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。
KVO是一种机制,它答应将其他对象的指定属性的更改通知给对象
在iOS官方文档中有这么一句话:
明白KVO之前,必须先明白KVC(即KVO是基于KVC基础之上)
   In order to understand key-value observing, you must first understand key-value coding.
KVC是键值编码,在对象创建完成后,可以动态的给对象属性赋值,而KVO是键值观察,提供了一种监听机制,当指定的对象的属性被修改后,则对象会收到通知,所以可以看出KVO是基于KVC的基础上对属性动态变化的监听
  我们知道NSNotificatioCenter也是一种监听方式,那么KVO与NSNotificatioCenter有什么区别呢?

2、都能实现一对多的利用

2、NSNotification的发送监听(post)的利用我们可以控制,kvo由系统控制。
3、KVO可以记录新旧值变化
一、KVO利用

1.基本利用

KVO的基本利用分为三步

  1. [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
复制代码

  1. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
  2.     if ([keyPath isEqualToString:@"name"]) {
  3.         NSLog(@"%@",change);
  4.     }
  5. }
复制代码

  1. [self.person removeObserver:self forKeyPath:@"nick" context:NULL];
复制代码
2.context利用

我们注意到这些方法中都有参数context,我们来讲解一下
context 参数的紧张作用是为 KVO 回调提供一个标识符或标记,这有助于区分同一属性上的不同观察者或在多个地方注册的同一个观察者。
在官方文档中,针对参数context有如下说明:

通俗的讲,context上下文紧张是用于区分不同对象的同名属性,从而在KVO回调方法中制止利用字符串进行区分,而是直接利用context进行区分,可以大大提拔性能,以及代码的可读性
因此我们可以知道,context常用于标识,从而区分
不同对象的同名属性
context利用总结

  1. //context的类型是 nullable void *,应该是NULL,而不是nil
  2. [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
复制代码

  1. //定义context
  2. static void *PersonNickContext = &PersonNickContext;
  3. static void *PersonNameContext = &PersonNameContext;
  4. //注册观察者
  5. [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
  6. [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
  7.    
  8.    
  9. //KVO回调
  10. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
  11.     if (context == PersonNickContext) {
  12.         NSLog(@"%@",change);
  13.     }else if (context == PersonNameContext){
  14.         NSLog(@"%@",change);
  15.     }
  16. }
复制代码
3.移除KVO通知的必要性

首先我们必要明白一下观察者与被观察者,比方下面这段代码:
  1. [self.person addObserver:self
  2.                   forKeyPath:@"name"
  3.                      options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
  4.                      context:nil];
复制代码
观察者将观察 Person 类的 name 属性的变化。在这个例子中,我们将利用 ViewController 作为观察者
在官方文档中,针对KVO的移除有以下几点说明

删除观察者时,请记住以下几点:

KVO注册观察者 和移除观察者是必要成对出现的,如果只注册,不移除,会出现类似野指针的崩溃,如下图所示

崩溃的原因是,由于第一次注册KVO观察者后没有移除,再次进入界面,会导致第二次注册KVO观察者,导致KVO观察的重复注册,而且第一次的通知对象还在内存中,没有进行释放,此时接收到属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,所以导致了类似野指针的崩溃,即一直保持着一个野通知,且一直在监听
其实简单来讲就是大概当我们推出视图控制器时,视图控制器已经被销毁,同时我们的观察者是视图控制器,但是我们的视图控制器仍旧是观察者,并没有被移除,因此当我们后续继续通过被观察者通知观察者时,就会出现观察者时已经被销毁的视图控制器,从而出现访问野指针的情况导致崩溃
4.KVO观察可变数组

KVO是基于KVC基础之上的,所以可变数组如果直接添加数据,是不会调用setter方法的,所有对可变数组的KVO观察下面这种方式不见效的,即直接通过[self.person.dateArray addObject“1”];向数组添加元素,是不会触发kvo通知回调的
在KVC官方文档中,针对可变数组的集合范例,有如下说明,即访问集合对象必要必要通过mutableArrayValueForKey方法,如许才能将元素添加到可变数组
  1.     [_t addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
  2.      [_t.array addObject:@1];
复制代码
如许不会出发通知,纵然数组元素改变
我们应该利用mutableArrayValueForKey方法
  1.     [_t addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew |
  2.     [[self.t mutableArrayValueForKey:@"array"] addObject:@"1"];
复制代码
二、代码调摸索索

1.KVO对属性观察

现在有一个属性与成员变量,分别注册KVO并且直接修改他们的值



发现只有age属性发生了变化

结论:
KVO只观察属性,不直接观察成员变量,这是由于setter方法的原因,但是利用KVC修改成员变量可以触发KVO
   KVO 通常只能观察通过属性的 setter 方法修改的属性。这是由于当您为某个属性添加观察者时,Objective-C
的运行时会动态创建该属性的一个特别子类,并在这个子类中重写 setter 方法来插入属性变化通知的代码。由于直接修改成员变量不会触发
setter 方法
,因此不会产生 KVO 通知。
  2.中间类

我们刚才提到了在运行时会创建一个中间类,接下来我们讲解一下这个中间类
根据官方文档所述,在注册KVO观察者后,观察对象的isa指针指向会发生改变
在注册观察者前后,对象的isa指针发生了变化

综上所述,在注册观察者后,实例对象的isa指针指向由kunkun类变为了NSKVONotifying_kunkun中间类,即实例对象的isa指针指向发生了变化

3.中间类的方法

既然天生了一个中间类,那么我们来查看一下这个中间类中有什么方法
  1. #pragma mark - 遍历方法-ivar-property
  2. - (void)printClassAllMethod:(Class)cls{
  3.     unsigned int count = 0;
  4.     Method *methodList = class_copyMethodList(cls, &count);
  5.     for (int i = 0; i<count; i++) {
  6.         Method method = methodList[i];
  7.         SEL sel = method_getName(method);
  8.         IMP imp = class_getMethodImplementation(cls, sel);
  9.         NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
  10.     }
  11.     free(methodList);
  12. }
  13. //********调用********
  14. [self printClassAllMethod:objc_getClass("NSKVONotifying_kunkun")];
复制代码
输出:

那么我们的父类也有一个setAge方法,那么这里的这个方法是继承还是重写呢?
我们接下来打印父类的方法列表看一下

从这里说明继承的方法不会在子类中显示,所以NSKVONotifying_kunkun重写了set方法
综上所述,有如下结论:

我们这里再来计划一个函数来验证中间类与类的关系
创建一个函数来遍历所有已注册的类,并查抄它们是否是指定类的子类。
  1. void PrintSubclassesOfClass(Class parentClass) {
  2.     int numClasses = objc_getClassList(NULL, 0);
  3.     Class *classes = NULL;
  4.     classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
  5.     numClasses = objc_getClassList(classes, numClasses);
  6.     for (int i = 0; i < numClasses; i++) {
  7.         Class cls = classes[i];
  8.         Class superClass = class_getSuperclass(cls);
  9.         
  10.         while (superClass) {
  11.             if (superClass == parentClass) {
  12.                 NSLog(@"%@ is a subclass of %@", NSStringFromClass(cls), NSStringFromClass(parentClass));
  13.                 break;
  14.             }
  15.             superClass = class_getSuperclass(superClass);
  16.         }
  17.     }
  18.     free(classes);
  19. }
  20. // 调用PrintSubclassesOfClass([_t class]);
复制代码

由此发现中间类是类的子类,用到了isa swizzling技术
3.dealloc中移除观察者后,isa指向是谁,以及中间类是否会销毁?

  1.     [_t addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
  2.     [_t removeObserver:self forKeyPath:@"age"];
复制代码
这两段代码实行后分别打印其isa指向

由此可见移除观察者后isa又变回了原来的指向
同时我们再次调用子类查找函数
  1.     [_t addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
  2.     [_t removeObserver:self forKeyPath:@"age"];
  3.     PrintSubclassesOfClass([_t class]);
复制代码
输出:

说明中间类仍旧存在没有被销毁
这里大概是考虑到重用的技术,后面再次注册观察者就不用重复天生中间类
总结

综上所述,关于中间类,有如下说明:

由此我们可以得到如下关系图

三、KVO本质

在前面铺垫了那么多,我们现在来讲讲KVO的实现流程
KVO的本质是改变setter方法的调用
首先我们知道了中间类重写了setter方法,我们来打印一下重写后的方法的IMP,也就是方法现实上会调用哪一个函数

当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
Foundation框架中另有许多比方_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函数。

GNUStep窥伺KVO源码

由于KVO的实现没有开源,因此我们无法查看KVO的源码
GNUStep是一个成熟的框架,实用于高级GUI桌面应用步伐和服务器应用步伐,它将Cocoa Objective-C软件库,以自由软件方式重新实现,可以或许运行在Linux和windows利用系统上。
GNUStep的Foundation与apple的API相同,固然具体实现大概不一样,但仍旧有鉴戒意义。
重写setter方法

GNUStep有一个模板类叫做GSKVOSetter,针对不同的数据范例,都有一个不同的setter方法实现,列举此中一个方法:
  1. - (void) setterChar: (unsigned char)val
  2. {
  3.   NSString  *key; // 定义一个用来存储属性名称的字符串
  4.   Class     c = [self class]; // 获取当前对象的类
  5.   // 定义一个函数指针,用来存储原始的 setter 方法的实现
  6.   void      (*imp)(id,SEL,unsigned char);
  7.   // 通过类和当前方法的选择器(_cmd),获取这个方法的原始实现,并转换为适当的函数指针类型
  8.   imp = (void (*)(id,SEL,unsigned char))[c instanceMethodForSelector: _cmd];
  9.   // 通过 _cmd 选择器获取与之关联的属性名,通常通过移除 set 前缀和小写化首字母实现
  10.   key = newKey(_cmd); // 这个 newKey 函数的实现没有给出,假设它能从 setter 名生成属性名
  11.   // 检查这个类是否为 key 提供自动 KVO 通知
  12.   // 这个检查是由 automaticallyNotifiesObserversForKey: 方法进行,该方法默认返回 YES
  13.   if ([c automaticallyNotifiesObserversForKey: key] == YES) // 通常总是返回 YES,除非在子类中被重写
  14.   {
  15.       [self willChangeValueForKey: key]; // 在改变值之前手动通知 KVO 系统属性即将变更
  16.       (*imp)(self, _cmd, val); // 调用原始的 setter 方法实现来更新属性值
  17.       [self didChangeValueForKey: key]; // 在改变值之后手动通知 KVO 系统属性已经变更
  18.   }
  19.   else
  20.   {
  21.       // 如果类表示不自动通知,则直接调用原始实现,不发送 KVO 通知
  22.       (*imp)(self, _cmd, val);
  23.   }
  24.   RELEASE(key); // 释放之前为 key 分配的内存(这个假设 key 是动态分配的,但代码中没有显示这部分)
  25. }
复制代码
由此我们可以知道重写后的setter方法的紧张步骤
  1.       [self willChangeValueForKey: key]; // 在改变值之前手动通知 KVO 系统属性即将变更
  2.       (*imp)(self, _cmd, val); // 调用原始的 setter 方法实现来更新属性值
  3.       [self didChangeValueForKey: key]; // 在改变值之后手动通知 KVO 系统属性已经变更
复制代码

我们用代码来验证一下调用顺序
  1. - (void)setAge:(int)age {
  2.         _age = age; // 直接赋值操作,确保使用下划线来访问实例变量,避免递归调用setter
  3.         NSLog(@"调用成功:已将 age 设置为 %d", _age); // 打印信息
  4. }
  5. - (void)willChangeValueForKey:(NSString *)key {
  6.     NSLog(@"willChangeValueForKey--begin");
  7.     [super willChangeValueForKey:key];
  8.     NSLog(@"willChangeValueForKey--end");
  9. }
  10. - (void)didChangeValueForKey:(NSString *)key {
  11.     NSLog(@"didChangeValueForKey--begin");
  12.     [super didChangeValueForKey:key];
  13.     NSLog(@"didChangeValueForKey--end");
  14. }
复制代码

符合我们上面所说的流程,同时在didChangeValueForKey方法中我们调用了observeValueForKeyPathfObject:change:context:,由此我们可以推测一下observeValueForKeyPathfObject:change:context:的实现代码

重写class方法

由于我们不想中间类袒露给用户,因此我们的步伐同时重写了中间类的class方法
  1. - (Class) class
  2. {
  3.   return class_getSuperclass(object_getClass(self));
  4. }
复制代码
由此我们class方法返回的就是原来的实例对象所属的类,而非中间类
重写delloc方法

  1. - (void) dealloc
  2. {
  3.   // Turn off KVO for self ... then call the real dealloc implementation.
  4.   [self setObservationInfo: nil];
  5.   object_setClass(self, [self class]);
  6.   [self dealloc];
  7.   GSNOSUPERDEALLOC;
  8. }
复制代码
- (void) dealloc对象释放后,移除KVO数据,将对象重新指向原始类
重写KVC方法

- (void) setValue: (id)anObject forKey: (NSString*)aKey这是KVC中的方法,但是在GNUStep中也重写了这个方法
  1. - (void) setValue: (id)anObject forKey: (NSString*)aKey
  2. {
  3.   Class     c = [self class];
  4.   void      (*imp)(id,SEL,id,id);
  5.   imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];
  6.   if ([[self class] automaticallyNotifiesObserversForKey: aKey])
  7.     {
  8.       [self willChangeValueForKey: aKey];
  9.       imp(self,_cmd,anObject,aKey);
  10.       [self didChangeValueForKey: aKey];
  11.     }
  12.   else
  13.     {
  14.       imp(self,_cmd,anObject,aKey);
  15.     }
  16. }
复制代码
这与我们上面讲到的重写后的setter方法类似,实现在原始类KVC调用前后添加[self willChangeValueForKey: aKey]和[self didChangeValueForKey: aKey],而这两个方法是触发KVO通知的关键。
所以说KVO是基于KVC的,而KVC正是KVO触发的入口。
成员变量利用KVC触发KVO

由此如果我们直接修改成员变量不会触发KVO,但是如果通过KVC修改成员变量就会触发KVO

  1. [_t setValue:@5 forKey:@"height"];
  2.     NSLog(@"@%d", _t->height);
复制代码

总结



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4