zoukankan      html  css  js  c++  java
  • KVO的使用及底层实现

    1、概念

    KVO(Key-Value-Observer)也就是观察者模式,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件,一般继承自NSObject的对象都默认支持KVO

    KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于:
    1、相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。也就是kvo监听到被观察属性值改变时只会通知到观察者,是一对一的关系。而通知模式则是在被观察值改变的时候发送全局通知,任何对象都可以接听到这个通知,是一个一对多的关系;
    2、KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。而通知需要在被监听对象改变的时候添加发送通知代码。

    2、使用

    1、

    //1.注册观察者
        /*
            - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
         
         observer:观察者  也就是被观察对象发生改变时通知的接收者
         
         keyPath:被观察的属性名   比如我们这里是age属性
         
         options:参数  这里一般选择NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld  也就是在回调方法里会受到被观察属性的旧值和新值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial枚举。
         
         context:这个参数可以传入任意类型的对象,这个值会传递到接收消息回调的代码中,是KVO中的一种传值方式。
    
         */
        [self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

    2、

    //2.实现通知回调方法 当被观察对象的属性值发生变化时  就会回调这个方法  change字典中存放KVO属性相关的值,根据options时传入的枚举来返回。
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"%@---%@----%@---%@",keyPath,object,change,context);
    }

    3、

      //3.移除监听
        [self.per1 removeObserver:self forKeyPath:@"age"];

    注意点

    KVOaddObserverremoveObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash

    苹果官方推荐的方式是,在init的时候进行addObserver,在deallocremoveObserver,这样可以保证addremove是成对出现的,是一种比较理想的使用方式。

    调用KVO属性对象时,不仅可以通过点语法和set语法进行调用,KVO兼容很多种调用方式:(关于KVC的实现原理接下来会讲到)

     // 1.通过属性的点语法间接调用
        self.per1.age = 123;
    //2. 直接调用set方法   [self.per1 setAge:123];
    // 3.使用KVC的setValue:forKeyPath:方法 [self.per1 setValue:@123 forKeyPath:@"age"]; //4. 使用KVC的setValue:forKey:方法 [self.per1 setValue:@123 forKey:@"age"]; // 5.通过mutableArrayValueForKey:方法获取到代理对象,并使用代理对象进行操作

    如果直接修改对象的成员变量是不会触发KVO的

    //PersonClass.h文件
    
    #import <Foundation/Foundation.h>
    @interface PersonClass : NSObject{
      @public;
    NSInteger _age;//成员变量 } //属性 @property (nonatomic, assign) NSInteger age; @end

    直接修改成员变量,我们发现没有触发KVO

     self.person1 -> _age = 234;

    上面全是监听一些基础的数据类型  当被观察属性是一个复杂对象时,比如现在person对象有一个属性animal,那么kvo会如何监听呢?

    #import <Foundation/Foundation.h>
    @class AnimalClass;
    @interface PersonClass : NSObject
    @property (nonatomic, assign) NSInteger age;
    @property (nonatomic, strong) AnimalClass *animal;
    
    @end

    AnimalClass类中有一个name属性

    @interface AnimalClass : NSObject
    @property (nonatomic, copy) NSString *name;
    @end

    当我们对animal这个属性进行监听时,发现当对animal的属性值(name)修改时  kvo并不会监听到,  而当给person对象重新赋值一个新的animalClass对象时会被监听到

        //会监听到改变  因为person1的animal属性是个指针 存储的是animal类型的一个地址值  当重新赋值一个alloc出来的新animalClass对象时  animal的地址值发生了改变  会调用person1的setAnimal方法
        AnimalClass *ani2 = [[AnimalClass alloc]init];
        ani2.name = @"cat";
        self.person1.animal = ani2;
        
        //不会被kvo监听到  因为修改animal的name属性 根本没有调用person1的setAnimal方法  只是调用了animal的setName方法
        self.person1.animal.name = @"cat";

    而当我们对person1.animal对象的name属性进行监听时  是可以监听到 self.person1.animal.name = @"cat";这种值改动的

        [self.person1.animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

    所以kvo能否监听到变化  要看这个被监听对象存储的是什么?实际上是否发生了改变?

    3、原理

    我们在通过runtime函数object_getclass分别打印person1在添加kvo前后的类对象分别是是PersonClass和NSKVONotifying_PersonClass;

    也就是在person1对象注册了kvo以后,其类对象发生了改变 

    我们在改变age值的时候  实际上是调用了setAge方法  而实例对象调用方法是根绝isa指针找到类对象的对象方法列表找到对应的方法进行调用,所以kvo的本质实际上是重写了被观察属性值的set方法

    NSKVONotifying_PersonClass类对象set方法的具体实现:(_NSSet*ValueAndNotify的内部实现)

    didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法

    实现原理

    KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。在运行时利用RuntimeAPI动态生成一个根据原类创建的中间类(命名规则是NSKVONotifying_xxx的格式),这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。

    首先重写set方法。在set方法里分别调用willChangeValueForKey->set的赋值操作->didChangeValueForKey  其中didChangeValueForKey在内部视线中会调用观察者的回调方法 返回被观察对象的相关参数

    并且将class方法重写,返回原类的Class(PersonClass类)。这是因为苹果不想暴露kvo的内部实现,建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

    _isKVOA方法,这个方法可以当做使用了KVO的一个标记,系统可能也是这么用的。如果我们想判断当前类是否是KVO动态生成的类,就可以从方法列表中搜索这个方法。 

     4、如何手动触发KVO

    KVO在属性发生改变时的调用是自动的,如果在被观察属性值没有改变的情况下手动调用kvo 那么需要时候调用willChangeValueForKey和didChangeValueForKey两个方法(两个方法必须都进行调用  系统在执行didChangeValueForKey方法前会检测willChangeValueForKey是否被调用了)

        [self.person1 willChangeValueForKey:@"age"];
        
        [self.person1 didChangeValueForKey:@"age"];

    手动触发的前提是这个对象已经添加了kvo  如果没有添加的话kvo是无法知道观察者是谁的 也就是不会回调观察者的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{}这个回调方法的

     参考资料 
  • 相关阅读:
    【转】php三种工具pecl pear composer的区别
    【转】Git使用详细教程
    【转】chrome Network 过滤和高级过滤
    Python3第三方组件最新版本追踪实现
    Python3版本号比较代码实现
    模糊测试工具设计思路浅谈
    Python3+WebSockets实现WebSocket通信
    Python3+PyCryptodome实现各种加密算法教程
    HTTP漫谈
    Python3+Robot Framework+RIDE安装使用教程
  • 原文地址:https://www.cnblogs.com/gaoxiaoniu/p/10695723.html
Copyright © 2011-2022 走看看