iOS 底层探索篇 —— KVO 底层原理(上)

523 阅读6分钟

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

1. KVO

KVO(Key Value Observing, 键值观察)是Objective-C对观察者模式的实现,每次当被观察对象的某个属性值发生改变时,注册的观察者便能获得通知

1.1注册一个kvo

在这里插入图片描述

  • observer:观察者 也就是被观察对象发生改变时通知的接收者
  • keyPath:被观察的属性名
  • options:参数 这里一般选择NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld 。也就是在回调方法里会受到被观察属性的旧值和新值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial枚举。
  • context:这个参数可以传入任意类型的对象,这个值会传递到接收消息回调的代码中,是KVO中的一种传值方式,可以当作标识符用来区分观察到的属性。

在这里插入图片描述

在这里插入图片描述

1.2移除一个kvo

在这里插入图片描述

注意

  • 移除观察者的时候如果他还没注册,那么就会报NSRangeException。注意要保持注册和移除的一对一关系,如果无法确定的话,就把removeObserver 放在try catch里面执行。
  • kvo在dealloc的时候不会自动移除,被观察的对象会一直给观察者发消息,即使观察者已经被释放了。如果给被释放的观察者发消息,那么就会有内存访问的错误。所以要在释放观察者之前要移除kvo。
  • protocols 没有提供询问物体是观察者还是被观察者的方法。为了避免释放的错误,一个典型的模式是在初始化的时候(例如在init或viewDidLoad中)注册为观察器,并在dealloc的时候移除观察者。要确保一个注册对应一个移除,并且确保观察者在被释放之前移除。

当这个对象没有移除观察者的时候,那么当self被释放之后,那么student就还会继续给self发送消息,这样就会导致崩溃

在这里插入图片描述

在这里插入图片描述

1.3手动和自动 kvo

automaticallyNotifiesObservers是控制手动和自动kvo的地方,当返回YES时候就是自动,返回NO就是手动

在这里插入图片描述

当返回NO的时候,需要在赋值之前调用willChangeValueForKey,赋值之后调用didChangeValueForKey,这样才会触发回调。

在这里插入图片描述

1.4 一对多

当要观察的属性被多个属性影响的时候,那么就可以调用setByAddingObjectsFromArray

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.5 可变数组观察

当观察可变数组的时候,这样的方法添加的object,是无法被kvo观察到的。

在这里插入图片描述

如果想要被kvo观察到,则需要mutableArrayValueForKey这个方法去添加。

在这里插入图片描述

添加后看到kind是2。

在这里插入图片描述

看到其对应的是Insertion,也就是插入。

在这里插入图片描述

2. KVO 原理

KVO是使用一种称为isa-swizzing的技术实现的,当观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向一个中间类而不是真正的类。

在注册的地方打个断点后运行。

在这里插入图片描述

输出person的class发现是LGPerson,往下走也就是注册了之后在重新输出,发现是一个叫NSKVONotifying_LGPerson的类。

在这里插入图片描述

所以可以知道,注册观察者的时候系统自动生成了一个NSKVONotifying_LGPerson类。为了证明是生成的而不是原先就存在的。重新运行,打下断点在注册观察者之前,发现NSKVONotifying_LGPerson不存在的,所以NSKVONotifying_LGPerson是动态生成的

在这里插入图片描述

那么NSKVONotifying_LGPersonLGPerson有什么关系呢? 用下列方法在注册观察者前后打印LGPerson类的子类,

在这里插入图片描述 在这里插入图片描述

运行后看到输出,发现NSKVONotifying_LGPerson是LGPerson的子类,而NSKVONotifying_LGPerson没有子类

在这里插入图片描述

2.1 NSKVONotifying_LGPerson

那么NSKVONotifying_LGPerson里面有什么东西呢? 用下面的方法打印出NSKVONotifying_LGPerson的方法列表

在这里插入图片描述

运行后发现,NSKVONotifying_LGPerson的方法有4个: setNickName:setter class:类型 dealloc :释放实例对象,isa重新指回去。 _isKVOA:是否KVO标识符。

在这里插入图片描述

这四个方法都是NSKVONotifying_LGPerson重写的而不是继承来的,可以打印LGStudent的方法列表来证明一下。发现什么都没有,说明如果是父类的就不会打印,所以NSKVONotifying_LGPerson里的方法都是重写的。

在这里插入图片描述

2.2 isa 指向

看到NSKVONotifying_LGPerson有dealloc,那么NSKVONotifying_LGPerson是否会释放呢?NSKVONotifying_LGPerson释放之后isa是否会指回LGPerson呢?

dealloc里面移除观察者然后打下断点后运行。

在这里插入图片描述

这里输出object_getClass(self.person),发现是NSKVONotifying_LGPerson,往下走之后重新输出object_getClass(self.person),发现输出结果成了LGPerson。这就代表着,isa重新指回了LGPerson

在这里插入图片描述

回到之前的viewController,然后点击屏幕触发之前写好的方法,看到还是有输出NSKVONotifying_LGPerson类,代表着NSKVONotifying_LGPerson还存在,没有被销毁

在这里插入图片描述

在这里插入图片描述

2.3 class 方法

重新进来,在添加观察者处后打下断点。

在这里插入图片描述

在lldb里面输出 self.person.class。按照正常的逻辑,现在self.person.class 输出的应该是NSKVONotifying_LGPerson,但是输出后发现是LGPerson。这里是因为NSKVONotifying_LGPerson只是用来帮忙做一些事情的,而明面上显示出来的还是LGPerson。

在这里插入图片描述

2.4 setter 方法

setter 方法是KVO的主要方法。setter存在,那么就排除了观察的是属性而不是成员变量,因为成员变量是没有setter的。 为成员变量添加观察者后运行。

在这里插入图片描述

点击屏幕,发现没有观察到Cooci,说明KVO是不观察成员变量的,同时也是对setter方法进行了监听在这里插入图片描述

那么这里调用的是谁的setter方法呢? 当把观察者移除后,发现还是可以调用setter方法,说明调用的是LGPerson的setter。

在这里插入图片描述

在这里插入图片描述

运行后再lldb为self->_person->_nickName打下断点。

在这里插入图片描述

往下走,触发setter方法后,发现进入了汇编。

在这里插入图片描述

lldb输出bt,发现确实调用了LGPerson 的setter,并且中间还调用了_NSSetObjectValueAndNotify_changeValueForKey。setNickName之所以被调用,就是因为中间的这些方法的影响。那么中间这些方法做了什么呢?

在这里插入图片描述

查看_NSSetObjectValueAndNotify方法,看到这里调用了willChangeValueForKeydidChangeValueForKey

在这里插入图片描述

往下看看到一个block的回调。

在这里插入图片描述

在看_changeValueForKey里面。发现里面做了键值观察,并且调用了NSKeyValueWillChangeNSKeyValueDidChange

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

observeValueForKeyPath回调处打下断点并触发回调,发现确实是从NSKeyValueDidChange里面来的,并且在NSKeyValueDidChange里面发送了一个通知。

在这里插入图片描述

3. 总结

  • 要保持注册和移除的一对一关系
  • 释放观察者之前要移除kvo。
  • automaticallyNotifiesObservers是控制手动和自动kvo的地方,当返回YES时候就是自动,返回NO就是手动。当返回NO的时候,需要在赋值之前调用willChangeValueForKey,赋值之后调用didChangeValueForKey,这样才会触发回调。
  • 当要观察的属性被多个属性影响的时候,那么就可以调用setByAddingObjectsFromArray。
  • 当观察可变数组的时候,需要mutableArrayValueForKey这个方法去添加。
  • KVO是使用一种称为isa-swizzing的技术实现的,当观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向一个中间类而不是真正的类。
  • 注册观察者的时候系统动态生成了一个NSKVONotifying_XXXXXX类,是XXXXXX类的子类
  • NSKVONotifying_XXXXXX类重写了class,dealloc,_isKVOA和setter方法
  • NSKVONotifying_XXXXXX类dealloc之后isa重新指回了LGPerson,NSKVONotifying_LGPerson还存在,没有被销毁。
  • KVO观察的是属性而不是成员变量