KVO分析

作者: Rachel_雷蕾 | 来源:发表于2020-10-28 20:01 被阅读0次

    上节研究完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<count; i++) {
            Method method = methodList[i];
            SEL sel = method_getName(method);
            IMP imp = class_getMethodImplementation(cls, sel);
            NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
        }
        free(methodList);
    }
    
    

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

    #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<count; i++) {
            if (cls == class_getSuperclass(classes[i])) {
                [mArray addObject:classes[i]];
            }
        }
        free(classes);
        NSLog(@"classes = %@", mArray);
    }
    

    调用


    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分析

          本文链接:https://www.haomeiwen.com/subject/dcdxvktx.html