iOS---14---KVO

作者: 清风烈酒2157 | 来源:发表于2021-02-02 09:13 被阅读0次

    [toc]

    什么是KVO

    KVO 全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO
    KVONSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
    KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVCmutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArrayNSSet

    KVO使用

    • 基础
    1. 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
    2. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
    3. 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash.
    • 注册
    1. 在注册观察者时,可以传入options参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial枚举。

    2. 还可以通过方法context传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式。

    3. 在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash

    • 监听方法
    1. 观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crashchange字典中存放KVO属性相关的值,根据options时传入的枚举来返回。枚举会对应相应key来从字典中取出值,例如有NSKeyValueChangeOldKey字段,存储改变之前的旧值。
    2. change中还有NSKeyValueChangeKindKey字段,和NSKeyValueChangeOldKey是平级的关系,来提供本次更改的信息,对应NSKeyValueChange枚举类型的value。例如被观察属性发生改变时,字段为NSKeyValueChangeSetting
    3. 如果被观察对象是集合对象,在NSKeyValueChangeKindKey字段中会包含NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement`的信息,表示集合对象的操作方式。
    • 兼容方法

      调用KVO属性对象时,不仅可以通过点语法和set语法进行调用,KVO兼容很多种调用方式。

     //点语法
        self.person.name  = @"null";
        //set方法
        [self.person setName:@"test"];
        // 数组变化
        [self.person.dateArray addObject:@"1"];
        //kpath
        [self.person setValue:@"111" forKeyPath:@"person.name"];
        // KVO 建立在 KVC
        [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
    
    • 注意点

      • KVOaddObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash`。
    • 苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证addremove是成对出现的,是一种比较理想的使用方式。

    KVO手动调用

    可以看到调用KVO主要依靠两个方法,在属性发生改变之前调用willChangeValueForKey:方法,在发生改变之后调用didChangeValueForKey:方法。

    - (void)setNick:(NSString *)nick{
        if([nick isEqualToString:_nick]){
            return;
        }
        [self willChangeValueForKey:@"nick"];
        _nick = nick;
        [self didChangeValueForKey:@"nick"];
    }
    

    如果想控制当前对象的自动调用过程,也就是由上面两个方法发起的KVO调用,则可以重写下面方法。方法返回YES则表示可以调用,如果返回NO则表示不可以调用。

    Return YES if the key-value observing machinery should automatically invoke -willChangeValueForKey:/-didChangeValueForKey:, -willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey:, or -willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects: whenever instances of the class receive key-value coding messages for the key, or mutating key-value coding-compliant methods for the key are invoked. Return NO otherwise. Starting in Mac OS 10.5, the default implementation of this method searches the receiving class for a method whose name matches the pattern +automaticallyNotifiesObserversOf[Key], and returns the result of invoking that method if it is found. So, any such method must return BOOL too. If no such method is found YES is returned.
    

    返回 BOOL 类型
    返回 YES的情况下,类的实例对象接收到 KVC 类型的实例方法时,如setValueForKey: 等,或变相的遵守KVC的该key 的访问器方法被调用时,如 setKey: 等,KVO 内部机制会自动调用 -willChangeValueForKey:, -didChangeValueForKey:等,自动发送 Change 通知。
    -willChangeValueForKey:, -didChangeValueForKey: 调用是触发通知的源头,这也就解释了手动发送 Change 通知时,为何需要写这两个方法。
    若不开启自动改变通知,则应返回 NO
    Mac OS 10.5 开始,这个方法的默认实现逻辑是:会先查找消息接受方的类是否有 key 匹配的 +automaticallyNotifiesObserversOf[Key] 方法,如:+automaticallyNotifiesObserversOfName,若有,则返回匹配方法的结果,若没有则返回 YES

    • 举例子
    + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
         BOOL automatic = NO;
         if ([key isEqualToString:@"nick"]) {
               automatic = NO;
           }else {
               automatic = [super automaticallyNotifiesObserversForKey:key];
           }
           return automatic;
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
        self.person.nick  = @"Nil”;
    }
    

    打印发现没有触发,
    如果set方法这样写

    - (void)setNick:(NSString *)nick{
        if([nick isEqualToString:_nick]){
            return;
        }
        [self willChangeValueForKey:@"nick"];
        _nick = nick;
        [self didChangeValueForKey:@"nick"];
    }
    

    打印有收到值改变

    KVO实现原理

    KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

      self.person = [[LGPerson alloc] init];
        [self printClasses:[LGPerson class]];
        [self printClassAllMethod:[LGPerson class]];
        // [self printClassAllMethod:[LGStudent class]];
        // [[LGStudent alloc] sayLove];
        
    
        [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    
        [self printClasses:[LGPerson class]];
        [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LGPerson")];
    

    打印发现

    2020-02-13 16:40:03.134467+0800 002---KVO原理探讨[2550:192081] classes = (
        LGPerson,
        LGStudent
    )
    2020-02-13 16:40:03.134705+0800 002---KVO原理探讨[2550:192081] *********************
    2020-02-13 16:40:03.134907+0800 002---KVO原理探讨[2550:192081] sayHello-0x10b96c540
    2020-02-13 16:40:03.135038+0800 002---KVO原理探讨[2550:192081] sayLove-0x10b96c550
    2020-02-13 16:40:03.135146+0800 002---KVO原理探讨[2550:192081] .cxx_destruct-0x10b96c590
    2020-02-13 16:40:03.135258+0800 002---KVO原理探讨[2550:192081] nickName-0x10b96c560
    2020-02-13 16:40:03.135391+0800 002---KVO原理探讨[2550:192081] setNickName:-0x10b96c4e0
    2020-02-13 16:40:53.117831+0800 002---KVO原理探讨[2550:192081] classes = (
        LGPerson,
        "NSKVONotifying_LGPerson",
        LGStudent
    )
    2020-02-13 16:40:53.118036+0800 002---KVO原理探讨[2550:192081] *********************
    2020-02-13 16:40:53.118152+0800 002---KVO原理探讨[2550:192081] setNickName:-0x10bcf0c7a
    2020-02-13 16:40:53.118260+0800 002---KVO原理探讨[2550:192081] class-0x10bcef73d
    2020-02-13 16:40:53.118420+0800 002---KVO原理探讨[2550:192081] dealloc-0x10bcef4a2
    2020-02-13 16:40:53.118550+0800 002---KVO原理探讨[2550:192081] _isKVOA-0x10bcef49a
    

    发现NSKVONotifying_LGPerson 新生成的类,已经不是之前的类了。KVO会在运行时动态创建一个新类,将对象的isa指向新创建的类,新类是原类的子类,命名规则是NSKVONotifying_xx的格式。KVO为了使其更像之前的类,还会将对象的class实例方法重写,使其更像原类。
    setNickName:-0x10bcf0c7a
    KVO会重写keyPath对应属性的setter方法,没有被KVO的属性则不会重写其setter方法。在重写的setter方法中,修改值之前会调用willChangeValueForKey:方法,修改值之后会调用didChangeValueForKey:方法,这两个方法最终都会被调用到observeValueForKeyPath:ofObject:change:context:方法中。

    • 调用willChangeValueForKey方法

    • 调用setAge方法

    • 调用didChangeValueForKey方法

    • didChangeValueForKey方法内部调用oberserobserveValueForKeyPath:ofObject:change:context:方法

    • 重写

    - (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
    }
    
    
    - (void)didChangeValueForKey:(NSString *)key{
    
    
    NSLog(@"didChangeValueForKey - begin");
    
    
    [super didChangeValueForKey:key];
    
    
    NSLog(@"didChangeValueForKey - end");
    }
    
    • 打印
    2020-02-13 16:58:58.975829+0800 002---KVO原理探讨[2675:204649] willChangeValueForKey
    2020-02-13 16:58:58.975941+0800 002---KVO原理探讨[2675:204649] didChangeValueForKey - begin
    2020-02-13 16:58:58.976213+0800 002---KVO原理探讨[2675:204649] {
        kind = 1;
        new = KC;
    }
    2020-02-13 16:58:58.976374+0800 002---KVO原理探讨[2675:204649] didChangeValueForKey - end
    
    

    KVO图示

    eb33d2dce29969ca1eb7f91731c48b71

    KVO面试题

    • 通过修改类的成员变量不会触发KVO,那为什么通过KVC的 给成员变量赋值会触发KVO呢?
      首先的基础是,我们看下的底层实现

    1、首先搜索setKey:方法.(key指成员变量名, 首字母大写)
    2、上面的setter方法没找到, 如果类方法返回YES. 那么按 _key, _isKey,key, iskey的顺序搜索成员名。
    如果找到了就会触发KVO,因为底层内部会调用 和方法。
    你可以重写该类的调用 和 方法去验证,当KVC改变属性值的时候,比如:Person继承NSObject,它有一个成员变量@public int _age;,[self.person1 setValue:@(10) forKey:@"age"]; 会调用 和 方法。所以会触发。
    通过修改类的成员变量不会触发KVO,因为成员变量不会生成setter方法,直接访问成员变量自然不会触发KVO,而要触发KVO本质是必须调用调用 和 方法。KVO的底层实现也是通过重写setter方法 setter方法里面调用和 方法。

    • 如何手动触发KVO
      1.手动调用willChangeValueForKeydidChangeValueForKey方法。

    • iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

    1. 利用RuntimeAPI动态生成一个子类NSKVONotifying_XXX,并且让instance对象的isa指向这个全新的子类NSKVONotifying_XXX
    2. 当修改对象的属性时,会在子类NSKVONotifying_XXX调用Foundation_NSSetXXXValueAndNotify函数
    3. _NSSetXXXValueAndNotify函数中依次调用 - 1、willChangeValueForKey- 2、父类原来的setter - 3、didChangeValueForKey,didChangeValueForKey:内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
    4. 如何手动触发KVO方法手动调用willChangeValueForKeydidChangeValueForKey方法键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和didChangeValueForKey。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了
      有人可能会问只调用didChangeValueForKey方法可以触发KVO方法,其实是不能的,因为willChangeValueForKey: 记录旧的值,如果不记录旧的值,那就没有改变一说了
    • 直接修改成员变量会触发KVO
      不会触发KVO,因为KVO的本质就是监听对象有没有调用被监听属性对应的setter方法,直接修改成员变量,是在内存中修改的,不走set方法
    • 不移除KVO监听,会发生什么
    1. 不移除会造成内存泄漏
    2. 但是多次重复移除会崩溃。系统为了实现KVO,为NSObject添加了一个名为NSKeyValueObserverRegistrationCategoryKVOaddremove的实现都在里面。在移除的时候,系统会判断当前KVOkey是否已经被移除,如果已经被移除,则主动抛出一个NSException的异常.

    相关文章

      网友评论

        本文标题:iOS---14---KVO

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