KVO分析

上节研究完KVC后,随之关联的还有一个KVO,本篇就让我们来分析一下KVO的使用以及原理

一、KVO使用

  • KVO通常的使用方法是addObserver forKeyPath
    image.png

    再使用回调函数处理结果
    image.png

    最后再dealloc:移除掉观察者

对于添加观察对象方法:addObserver,可以根据官方文档KVO官方查询相关用法,其中

  • addObserver:观察对象:观察对象首先通过发送广告向被观察对象注册
  • keypath:需要观察的值,只能是被观察对象的属性值
  • options:参数指定为按位或选项常量:即是被观察属性的变化,来影响生成通知的方式。有下面几种方式

NSKeyValueObservingOptionNew:观察属性的新值
NSKeyValueObservingOptionOld:选择从更改之前接收观察到的属性的值
NSKeyValueObservingOptionInitial:可以使用此附加的一次性通知在观察者中建立属性的初始值
NSKeyValueObservingOptionPrior:可以指示观察到的对象在属性更改之前发送通知(除了更改之后的常规通知)。变更字典表示变更前通知,方法是将键NSKeyValueChangeNotificationIsPriorKey与包装为YES的NSNumber的值包含在一起

  • context:上下文,是标记不同对象或者不同属性的作用。因为同一个文件里可能有多个被观察对象,或者一个观察对象有多个属性值被观察,使用 静态变量的地址(ex:static void *PersonNickContext = &PersonNickContext;形式来分辨不同的对象或不同的属性。方便在回调函数中确定对象或者属性来进行后续操作。增加了代码的可读性,可扩展性,安全性
  • addObserver: forKeyPath :options:context:方法不维护对观察对象、观察对象或上下文的强引用,因为是存在弱引用表中
  • dealloc:每次调用addObserver后,都要调用dealloc方法,在其中实现
    移除键值观察器消息,指定观察对象路径上下文:(ex: [self.person removeObserver:self forKeyPath:@"nick" context:NULL]);

注:
1、如果没有注册观察员,则请求将其删除为观察员会导致NSRangeException
2、释放时,观察者不会自动删除自身。被观察对象继续发送通知,而不考虑观察者的状态。但是,与任何其他消息一样,发送到已发布对象的更改通知会触发内存访问异常。因此,你要确保观察者在从内存中消失之前将自己移除。
3、没有提供询问对象是观察者还是被观察者的方法。构造代码以避免相关错误。一种典型的模式是在观察者初始化期间(例如在init或viewdiload中)注册为观察者,并在释放期间注销(通常在dealoc中),确保正确配对和有序地添加和删除消息,并且在将观察者从内存中释放之前取消注册。

-是否可以 不移除:不可以。否则会崩溃,观察对象没被移除,但是观察者已经被释放了,再次注册时,添加观察器,消息发送后,系统不知道应该由哪个观察器接受。造成指针混乱(
由于第一次注册KVO观察者后没有移除,再次进入界面,会导致第二次注册KVO观察者,导致KVO观察的重复注册,而且第一次的通知对象还在内存中,没有进行释放,此时接收到属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,所以导致了类似野指针的崩溃,即一直保持着一个野通知,且一直在监听)

image.png
  • 自动/手动接受消息
    默认观察器都是自动接受到属性值变化的消息的。如果想要手动调用,则要关闭自动开关automaticallyNotifiesObserversForKey

    image.png

    并且在属性的set方法中实现willChangeValueForKeydidChangeValueForKey,并在其中间赋值
    image.png

    打开自动开关( 默认)时,猜测系统检测属性值变化,也是调用了willChangeValueForKeydidChangeValueForKey
    测试:
    1、首先在addObserve处打断点,使用watchpoint set variable self->_person->_nick来观察属性值nick(注:watchpoint set variable:观察变量值改变命令)
    image.png

    2、然后点击页面,捕捉到nick的变化
    d

    3、最终调入如下,堆栈显示
    image.png

    结论:属性值变化时,确实是willChangeValueForKeydidChangeValueForKey之间捕捉了nickset方法

  • 覆写keypath:通过覆写keypath来定一个新的观察路径。
    使用案例:
    检测进度:定义一个进度属性:downloadProgress以及相关属性:

    image.png

    覆写keypath
    image.png

    设置初始值
    image.png

    注册观察器
    image.png

    检测keypath变化
    image.png

  • 检测数组变化
    定义一个可变数组:dateArray,点击页面时赋值,发现,在回调方法不走。这是为什么呢?

    image.png

找到文档里关于观察数组时的要求:观察数组要按照kcv的形式赋值,才能发送更改消息
那么将数组按照kvc形式赋值,更改


image.png

结果收到了更改的消息,且类型kind为2,是NSKeyValueChange的值,查到NSKeyValueChange定义,有如下四种值的改变方式

image.png

二、KVO底层
都知道,KVO只能观察属性,不能观察成员变量,这个也有在代码里验证过,只能是属性可以被观察,这是为什么呢,属性和成员变量的区别就在于,多了setget方法。说明,是kvo
只能观察set方法,捕捉到了值的变化,下面让我们来验证

  • 根据文档,被观察对象的isa会指向一个中间类
    这个中间类是什么,又是在什么时节生成的?
    观察生成中间类时机,测试一下:
    image.png

    果然,调用完addObserver后,生成了一个中间类,也可以叫做派生类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

顺便添加一下打印类名的方法

#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
    
    // 注册类以及它子类的名字
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i

调用


image.png

发现派生类实际上是当前类的子类,且重新生成set方法

注:为什么方法继承:因为继承的话,不会在子类中显示,只在父类中,这一点也可以添加LGPerson类的一个子类再打印一次方法列表试试,对比结果,也可以印证这一点。

  • 何时isa再指回原类,猜测是在dealloc里,移步到dealloc
    测试:
    image.png

确实调用完removeObserver后,isa再指回原类了。

  • ️:派生类已经移除了么?
    我们到dealloc里处理打断点,移除观察者后,再调用获取类和子类的方法,发现派生类还存在。ps:测试页面销毁后,再次获取person类及其子类,还是同样的结果。

image.png

派生类根本就不移除了:因为KVO派生类只要生成,就会一直存在,这样可以减少频繁的添加操作

至此,整个KVO原理大致流程明白了:创建派生类实现了键值观察。

添加:addObserver时,创建了派生类,派生类是当前类的子类重写了被监听属性的setter方法,并将当前类的isa指向了派生类。(此时开始,所有调用本类的方法,都是调用的派生类。派生类中没有的方法,就会沿着继承链查询到本类)

改变属性值: 派生类重写了被监听属性的setter方法,在派生类的setter方法触发时:在willChange之后didChange之前,调用父类属性setter方法,完成父类属性的赋值。

移除: 在removeObserver后,isa派生类指回本类。 但创建过的派生类会被本类从子类列表中移除,会一直存在。

假象: 外部打印class永远看不到派生类,是因为派生类将class方法重写了,故意不让外界看到。

你可能感兴趣的:(KVO分析)