【iOS】KVO

王柳  金牌会员 | 2024-6-21 13:11:47 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 796|帖子 796|积分 2388


前言

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有什么区别呢?


  • 相同点:
    1、两者的实现原理都是观察者模式,都是用于监听
2、都能实现一对多的利用


  • 不同点:
    1、KVO监听对象属性的变化,同时只能通过NSString来查找属性名,较容易堕落
2、NSNotification的发送监听(post)的利用我们可以控制,kvo由系统控制。
3、KVO可以记录新旧值变化
一、KVO利用

1.基本利用

KVO的基本利用分为三步


  • 注册观察addObserver:forKeyPathptions:context
  1. [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
复制代码


  • 实现KVO回调observeValueForKeyPathfObject:change:context
  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. }
复制代码


  • 移除观察者removeObserver:forKeyPath:context
  1. [self.person removeObserver:self forKeyPath:@"nick" context:NULL];
复制代码
2.context利用

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

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


  • 不利用context,利用keyPath区分通知来源
  1. //context的类型是 nullable void *,应该是NULL,而不是nil
  2. [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
复制代码


  • 利用context区分通知来源
  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的移除有以下几点说明

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


  • 要求被移除为观察者(如果尚未注册为观察者)会导致NSRangeException。您可以对removeObserver:forKeyPath:context:进行一次调用,以对应对addObserver:forKeyPath:options:context:的调用,或者,如果在您的应用中不可行,则将removeObserver:forKeyPath:context:调用在try / catch块内处置处罚潜在的非常。
  • 释放后,观察者不会自动将其自身移除。被观察对象继续发送通知,而忽略了观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问非常。因此,您可以确保观察者在从内存中消失之前将本身删除。
  • 该协议无法询问对象是观察者还是被观察者。构造代码以制止发布相干的错误。一种典型的模式是在观察者初始化期间(比方,在init或viewDidLoad中)注册为观察者,并在释放过程中(通常在dealloc中)注销,以确保成对和有序地添加和删除消息,并确保观察者在注册之前被取消注册,从内存中释放出来。
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方法
综上所述,有如下结论:


  • NSKVONotifying_kunkun中间类重写了父类kunkun的setAge方法
  • NSKVONotifying_kunkun中间类重写了基类NSObject的class 、 dealloc 、 _isKVOA方法
    此中dealloc是释放方法
    _isKVOA判断当前是否是kvo类
我们这里再来计划一个函数来验证中间类与类的关系
创建一个函数来遍历所有已注册的类,并查抄它们是否是指定类的子类。
  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]);
复制代码
输出:

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

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


  • 实例对象isa的指向在注册KVO观察者之后,由原有类更改为指向中间类
  • 中间类重写了观察属性的setter方法、class、dealloc、_isKVOA方法
  • dealloc方法中,移除KVO观察者之后,实例对象isa指向由中间类更改为原有类
  • 中间类从创建后,就一直存在内存中,不会被销毁
由此我们可以得到如下关系图

三、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 系统属性已经变更
复制代码


  • 先调用willChangeValueForKey方法,
  • 再调用父类原来的setter方法
  • 最后调用didChangeValueForKey,其内部会触发监听器(Oberser)的监听方法(observeValueForKeyPathfObject:change:context:);
我们用代码来验证一下调用顺序
  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);
复制代码

总结



  • KVC是KVO的入口,网上许多人说成员变量无法被KVO观察,其实是可以的,只是必要调用KVC,但是口试时一样平常都会说KVO只能用来观察属性
  • KVO的实现紧张就是通过isa swizzling技术交换isa指针,在运行时天生中间类,在中间类中重写setter方法从而通知触发KVO监听函数。
  • 重写后的setter方法调用顺序紧张为willChangeValueForKey->setter方法->didChangeValueForKey
  • 同时移除观察者后中间类会一直存在等待重用
  • 参考博客
    iOS底层原理总结 - 探寻KVO本质
    KVO源码浅析
    iOS-底层原理 23:KVO 底层原理

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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

王柳

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表