美文网首页
KVO(Key-Value Observing)

KVO(Key-Value Observing)

作者: Kevin_ | 来源:发表于2021-06-08 17:04 被阅读0次

    一、KVO简介

    KVOObjective-C 对观察者模式(Observer Pattern)的实现,也是 Cocoa Binding 的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知。

    二、KVO的基本使用

    1. 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件
     /*
    @observer:观察者
    @keyPath:想要观察的对象属性
    @options:options一般选择NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,这样当属性值发生改变时我们可以同时获得旧值和新值,如果我们只填NSKeyValueObservingOptionNew则属性发生改变时只会获得新值
    @context:想要携带的其他信息
    */
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
    
    1. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者
    /*
    @keyPath:观察的属性
    @object:观察的是哪个对象的属性
    @change:这是一个字典类型的值,通过键值对显示新的属性值和旧的属性值
    @context:添加观察者时携带的信息
    */
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
    
    1. 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。注意调用removeObserver需要在观察者消失之前,否则会导致Crash
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    

    三、KVO实现机制

    KVO是通过isa混写(isa-swizzling)技术实现。

    1. 当观察一个对象时,一个新的类会动态被创建(动态添加的类名就是在原来类的类名前加上NSKVONotifying_类名);
    2. 这个类继承自该对象原本的类,并重写被观察属性的 setter 方法
    3. 重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象值的更改
    4. 最后把这个对象的 isa指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就变成了新创建的子类的实例。

    四、KVO的自动触发与手动触发

    KVO观察的开启和关闭有两种方式,自动手动
    自动开关,返回NO,就监听不到,返回YES,表示监听;对于想要手动通知的属性,可以根据它的keyPath返回NO,而其对于其他位置的keyPath,要返回父类的这个方法。

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
           BOOL automatic = NO;
           if ([theKey isEqualToString:@"openingBalance"]) {
               automatic = NO;
           } else {
               automatic = [super automaticallyNotifiesObserversForKey:theKey];
           }
           return automatic;
    }
    

    要实现手动通知,你需要在值改变前调用 willChangeValueForKey:方法,在值改变后调用 didChangeValueForKey: 方法。你可以在发送通知前检查值是否改变,如果没有改变就不发送通知。

    - (void)setOpeningBalance:(double)theBalance {
           if (theBalance != _openingBalance) {
            [self willChangeValueForKey:@"openingBalance"];
            _openingBalance = theBalance;
            [self didChangeValueForKey:@"openingBalance"];
           }
    }
    

    使用手动开关的好处就是你监听就监听,不想监听关闭即可,比自动触发更方便灵活

    如果一个操作会导致多个属性改变,你需要嵌套通知,像下面这样:

    - (void)setOpeningBalance:(double)theBalance {
           [self willChangeValueForKey:@"openingBalance"];
           [self willChangeValueForKey:@"itemChanged"];
           _openingBalance = theBalance;
           _itemChanged = _itemChanged+1;
           [self didChangeValueForKey:@"itemChanged"];
           [self didChangeValueForKey:@"openingBalance"];
    }
    

    在一个一对多的关系中,你必须注意不仅仅是这个key改变了,还有它改变的类型以及索引。

    - (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
           [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
    
           // Remove the transaction objects at the specified indexes.
    
           [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
    }
    

    五、KVO一对多,键值依赖

    通过注册一个KVO观察者,可以监听多个属性的变化

    比如目前有一个需求,需要根据总的下载量totalData 和当前下载量currentData 来计算当前的下载进度currentProcess,实现有两种方式

    • 分别观察totalDatacurrentData 两个属性,当其中一个发生变化计算currentProcess
    • 实现keyPathsForValuesAffectingValueForKey方法,将两个观察合为一个观察,即观察当前下载进度currentProcess
    //1、合二为一的观察方法
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
        
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"currentProcess"]) {
            NSArray *affectingKeys = @[@"totalData", @"currentData"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }
    
    //2、注册KVO观察
    [self.person addObserver:self forKeyPath:@"currentProcess" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    //3、触发属性值变化
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.person.currentData += 10;
        self.person.totalData  += 1;
    }
    
    //4、移除观察者
    - (void)dealloc{
        [self.person removeObserver:self forKeyPath:@"currentProcess"];
    }
    

    你也可以通过实现 keyPathsForValuesAffecting<Key> 方法来达到前面同样的效果,这里的<Key>就是属性名,不过第一个字母要大写,用前面的例子来说就是这样:

    + (NSSet *)keyPathsForValuesAffectingCurrentProcess {
        return [NSSet setWithObjects:@"totalData", @"currentData", nil];
    }
    

    六、KVO观察 可变数组

    KVO是基于KVC基础之上的,所以可变数组如果直接添加数据,是不会调用setter方法的,即直接通过[self.person.dateArray addObject:@"1"];向数组添加元素,是不会触发KVO通知回调的

    在KVC官方文档中,针对可变数组的集合类型,有如下说明,即访问集合对象需要需要通过mutableArrayValueForKey方法,这样才能将元素添加到可变数组中;

    // KVC 用此方法添加则可以触发KVO
        [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
    

    七、其他知识点

    • 通过KVC修改属性会触发KVOKVC 内部做了监听操作
    • 直接修改成员变量不会触发 KVO,没走set方法
    • 哪些情况下使用kvo会崩溃,怎么防护崩溃?

    1.removeObserver一个未注册的keyPath,导致错误:Cannot remove an observer A for the key path "str",because it is not registered as an observer.
    解决办法:根据实际情况,增加一个添加keyPath的标记,在dealloc中根据这个标记,删除观察者。
    2.添加的观察者已经销毁,但是并未移除这个观察者,当下次这个观察的keyPath发生变化时,kvo中的观察者的引用变成了野指针,导致crash
    解决办法:在观察者即将销毁的时候,先移除这个观察者。
    其实还可以将观察者observer委托给另一个类去完成,这个类弱引用被观察者,当这个类销毁的时候,移除观察者对象

    • kvo的优缺点

    优点:
    1.能够提供一种简单的方法实现两个对象间的同步
    2.能够对非我们创建的对象,即内部对象的状态改变做出响应,而且不需要改变内部对象的实现
    3.能够提供观察的属性的最新值以及先前值
    4.用key paths来观察属性,因此也可以观察嵌套对象
    5.完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
    缺点:
    1.我们观察的属性必须使用string来定义,因此在编译期不会出现警告以及检查

    2.对属性重构将导致我们的观察代码不再可用
    3.只能通过重写 -observeValueForKeyPath:ofObject:change:context:方法来获得通知。
    4.不能通过指定selector的方式获取通知。
    5.不能通过block的方式获取通知。

    • 添加观察者和移除观察者要相对应;
    • 不要将已经释放的观察者对象,再进行移除;
    • 可以多次对同一个属性添加相同的观察者,当属性更改的时候,会多次调用接收方法,不过移除观察者也要执行多次;
    • 在iOS10及其以下,不移除观察者会出现闪退的情况,在iOS11及其以上,不会出现闪退的情况;

    相关文章

      网友评论

          本文标题:KVO(Key-Value Observing)

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