美文网首页
正确使用KVO的姿势

正确使用KVO的姿势

作者: Daved | 来源:发表于2018-01-28 10:17 被阅读88次

    使用KVO的前提条件

    1. 该类必须满足KVC命名约定查看此处
    2. 该类可以触发属性变更的KVO通知继承自NSObject的类默认由NSObject实现该功能
    3. 依赖的属性被正确的注册到KVO。如:fullName依赖lastNamefirstName

    使用方式

    1. 添加观察者:addObserver:forKeyPath:options:context:,如:需要观察Account对象的balance属性
    static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
    
    [account addObserver:self
                  forKeyPath:@"balance"
                     options:(NSKeyValueObservingOptionNew |
                              NSKeyValueObservingOptionOld)
                     context:PersonAccountBalanceContext];
    
    1. 观察回调方法处理:observeValueForKeyPath:ofObject:change:context:
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context {
        
        if (context == PersonAccountBalanceContext && [keyPath isEqualToString:@"balance"]) {
            
            NSLog(@"Do something with the balance…");
            
        } else {
            // Any unrecognized context must belong to super
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                  context:context];
        }
    }
    
    1. 移除观察者:removeObserver:forKeyPath:context:
    [account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
    

    以上是我们常见的使用KVO的方式,在具体实践中还有许多坑要踩,接下来我们逐个探讨。

    进阶使用

    截止目前我们使用KVO的方式都是依赖系统实现的自动触发机制,在有些情况下我们需要更精确的控制KVO的触发时机,此时需要手动触发KVO。

    1. 手动触发KVO

    手动触发KVO需要覆写NSObjectautomaticallyNotifiesObserversForKey:方法。该方法默认返回YES,调用自动触发KVO的逻辑。对需要手动触发KVO的属性需要变更该方法的返回值为NO。

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        BOOL automatic = NO;
        
        // 手动触发balance属性KVO
        if ([key isEqualToString:@"balance"]) {
            automatic = NO;
        } else {
        
            // 其它属性KVO自动触发
            automatic = [super automaticallyNotifiesObserversForKey:key];
        }
        return automatic;
    }
    

    实现手动触发KVO通知还需要在属性值变更前调用willChangeValueForKey:变更后调用didChangeValueForKey:

    - (void)setBalance:(double)theBalance {
        if (theBalance != _balance) {
            [self willChangeValueForKey:@"balance"];
            _balance = theBalance;
            [self didChangeValueForKey:@"balance"];
        }
    }
    

    从OS X 10.5开始automaticallyNotifiesObserversForKey:会在被观察的类中查找+automaticallyNotifiesObserversOf<Key>方法,其中<key>表示被观察类的属性。以balance属性为例,实现automaticallyNotifiesObserversOfBalance即可,此时不需要再覆写automaticallyNotifiesObserversForKey:

    + (BOOL)automaticallyNotifiesObserversOfBalance {
        return NO;
    }
    
    1. 依赖属性KVO

    在许多情况下一个属性需要依赖其它属性值,例如:fullName是由firstNamelastName组成。

    - (NSString *)fullName {
        return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
    }
    

    假设我们需要对fullName做观察,当firstNamelastName有变化时自动更新fullName的值并触发KVO通知。

    实现依赖属性KVO有以下两种方式。

    • 实现keyPathsForValuesAffectingValueForKey:方法
    + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        
        if ([key isEqualToString:@"fullName"]) {
            NSArray *affectingKeys = @[@"lastName", @"firstName"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }
    
    • 实现keyPathsForValuesAffecting<Key>方法,其中<key>表示被观察对象的属性
    + (NSSet *)keyPathsForValuesAffectingFullName {
        return [NSSet setWithObjects:@"firstName",@"lastName", nil];
    }
    
    • 注意事项:在category中依赖属性观察不能覆写keyPathsForValuesAffectingValueForKey:方法,因为category中不能覆写方法。在category中可以通过实现keyPathsForValuesAffecting<Key>方法实现依赖属性观察。

    注意事项

    1. keypath使用方式优化

    截止目前我们都是以字符串的方式使用keypath,这种方式使得编译器在编译期间不能及时的发现错误,一种比较好的方式是通过NSStringFromSelector@selector结合的方式来使用keypath,如下所示。

     [self addObserver:self
                   forKeyPath:NSStringFromSelector(@selector(fullName))
                      options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                      context:FullNameContext];
    
    1. Context使用

    在使用KVO时有一个关键点就是在添加观察者时传入一个唯一的context,如下所示。

    // 观察fullName的context
    static void *FullNameContext = &FullNameContext;
    ...
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context {
        
         if (context == FullNameContext && [keyPath isEqualToString:NSStringFromSelector(@selector(fullName))]) {
            // do somthing with fullName
        } else {
            // Any unrecognized context must belong to super
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                  context:context];
        }
    }
    
    

    这样会保证我们观察的子类都是正确的,子类和父类都能安全的观察同样的键值而不会冲突。否则我们将会碰到难以 debug 的奇怪行为。

    安全的移除观察者

    当一个观察者完成了监听对象的改变使命,需要调用–removeObserver:forKeyPath:context:来移除观察,否则会出现程序崩溃的情况。
    当重复移除观察者时同样会导致程序崩溃。

    目前还没有公开的API来检测一个对象是否被注册为观察者。
    要安全的移除观察者可以通过以下两种方式来实现。

    1. try/catch
    - (void)dealloc {
       
        @try {
            [self removeObserver:self
                      forKeyPath:NSStringFromSelector(@selector(fullName))
                         context:FullNameContext];
        } @catch (NSException * exception) {
            
        }
    }
    
    1. 遍历observationInfo

    通过查阅API我们发现NSObject有一个observationInfo属性,官方文档对该属性的描述如下。

    Returns a pointer that identifies information about all of the observers that are registered with the observed object.

    基于此可以通过KVC的方式获取到对象是否注册相关keypath的观察者

    observationInfo结构
    // 按key检索
    - (BOOL)observerKeyPath:(NSString *)key observer:(id)observer
    {
        id info = self.observationInfo;
        NSArray *array = [info valueForKey:@"_observances"];
        for (id objc in array) {
            id Properties = [objc valueForKeyPath:@"_property"];
            id newObserver = [objc valueForKeyPath:@"_observer"];
            
            NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
            if ([key isEqualToString:keyPath] && (newObserver == observer)) {
                return YES;
            }
        }
        return NO;
    }
    

    实现原理

    KVO的实现官方文档中提到使用了isa-swizzling技术,实现思路如下

    当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。

    并未透漏更多细节内容。
    关于KVO实现细节的探究可以参考KVO实现原理

    优雅的使用KVO

    关于KVO被吐槽最多的就是其晦涩的API和使用方式,如何解决这个问题呢,可以使用Facebook开源的KVOController,使用方式如下

    // create KVO controller with observer
    FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
    self.KVOController = KVOController;
    
    // observe clock date property
    [self.KVOController observe:clock 
                        keyPath:@"date"
                        options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew 
                        block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
    
            // update clock view with new value
            clockView.date = change[NSKeyValueChangeNewKey];
    }];
    

    使用KVOContoller解决了以下问题。

    • 不需要再手动移除观察者
    • 使用Block方式降低接口使用复杂度
    • 不再需要if判断keypath

    参考

    Key-Value Observing Programming Guide

    Key-Value Observing

    KVO和KVC

    如何优雅地使用 KVO

    KVO实现原理

    相关文章

      网友评论

          本文标题:正确使用KVO的姿势

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