KVO in iOS

作者: 37b32340bcbc | 来源:发表于2016-11-30 16:00 被阅读0次

    复习了一些iOS里大神写的KVO官方文档翻译和其他的博客,记录下来一些方便自己以后回来看。

    <NSKeyValueObserving>或者KVO,是一个非正式协议,它定义了对象之间观察和通知状态改变的通用机制。

    基本使用

    使用KVO必须要满足的条件和一般使用步骤:

    1.该对象必须支持KVC(凡是继承自NSObject的类都支持KVC)
    2.作为观察者的对象必须实现 -(void)observeValueForKeyPath:ofObject:change:context: 方法
    3.被观察的对象要用- (void)addObserver:forKeyPath:options:context:方法注册观察者
    4.用完要移除。附上方法- (void)removeObserver:forKeyPath: 或者- (void)removeObserver:forKeyPath:context:
    

    关于这几个方法里面的参数,需要一个一个说明。从-(void)observeValueForKeyPath:ofObject:change:context:方法开始。

     //keyPath:被观察的属性
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath
    //object:被观察的属性所属的对象
    ofObject:(nullable id)object 
    //change:这是一个字典,它包含了属性被修改的一些信息。
    //这个字典中包含的值会根据我们在添加观察者时(addObserver方法)设置的options参数有所变化。
    change:(nullable NSDictionary<NSString*, id> *)change
    //context:添加观察者时的上下文信息,它可以被用作区分那些绑定同一个keypath的不同对象的观察者。
    //比如说观察一些继承自同一个父类的子类,而这些子类都有一个相同的keyPath。
    context:(nullable void *)context;
    

    关于change字典里面的键值对,系统提供了这些预定义的key供我们使用

    NSKeyValueChangeKindKey 可以用@"kind"替代,也就是change[NSKeyValueChangeKindKey]等价于change[@"kind"]
    NSKeyValueChangeNewKey 可以用@"new"替代
    NSKeyValueChangeOldKey 可以用@"old"替代
    NSKeyValueChangeIndexesKey 可以用@"indexes"替代
    NSKeyValueChangeNotificationIsPriorKey 可以用@"notificationIsPrior"替代
    

    change字典里面会有哪些key出现取决于在addObserver方法中options参数的设置情况。(如果有人在看这篇文章的话建议先看下面addObserver方法参数和NSKeyValueObservingOptions的那部分内容,然后再回来看这段,因为这里的key和options关联很大。原谅我- -||)NewKey和OldKey很简单,就是options设置NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld时会在change里加入的键值对。

    NSKeyValueChangeNotificationIsPriorKey是在设置了NSKeyValueObservingOptionPrior选项后当被观察的值将要改变(但是还未改变)时发送的通知里会有的key,对应的是一个布尔值。

    NSKeyValueChangeKindKey对应的value是一个枚举值(NSKeyValueChange,就是下面这个),当被观察的值被设置时(setter方法调用时)KindKey对应的值为1(NSKeyValueChangeSetting)。
    如果观测的值是一个可变数组,那么当数组执行插入,删除,替换时kindKey会对应Insertion,Removal和Replacement。

    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
    };
    

    NSKeyValueChangeIndexesKey:当NSKeyValueChangeKindKey对应了2/3/4这几个值得时候,这个key的value是一个NSIndexSet,包含了发生insert,remove,replace的对象的索引集合。如果这个时候打印一下change字典大概会看到里面这样的一个键值对。

    indexes = "<_NSCachedIndexSet: 0x7fde0a50cd20>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    

    然后是- (void)addObserver:forKeyPath:options:context:方法,这个方法在调用时,观察者和被观察者对象的引用计数都不会增加。也就是在对象被释放之后,如果KVO的监听信息依然存在的话会导致程序崩溃。所以在适当的时候要记得使用removeObserver方法将观察者信息remove掉。

    //observer:观察者对象,也就是实现了observeValueForKeyPath:ofObject:change:context:方法的对象
    - (void)addObserver:(NSObject *)observer 
    //keyPath:被观察的属性
    forKeyPath:(NSString *)keyPath 
    //options:监听选项,这个值可以是NSKeyValueObservingOptions选项的组合
    //也就是可以这么写(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
    options:(NSKeyValueObservingOptions)options 
    //context:同上面的方法
    context:(nullable void *)context;
    

    关于NSKeyValueObservingOptions,里面一共有四个值:

    //设置后会在observeValueForKeyPath方法的change字典里存入更新后的值。
    NSKeyValueObservingOptionNew
    //设置后会在observeValueForKeyPath方法的change字典里存入更新前的值,也就是原有的值。
    NSKeyValueObservingOptionOld
    //设置后会在添加观察者的时候立即发送一次通知给观察者,并且在注册观察者方法之前返回。
    //也就是在addObserver方法执行之后就立即发送了一次通知。
    NSKeyValueObservingOptionInitial
    //会在值被改变之前发送一次通知,并且在change字典里多了一个叫notificationIsPrior的key,值是1。
    //而且change字典不会包含new(NSKeyValueChangeNewKey)这个key。
    //当然值改变后的那次通知也会发,也就是说会发送两次通知。
    NSKeyValueObservingOptionPrior
    

    当观察者不再需要监听属性变化时,需要使用- (void)removeObserver:forKeyPath: 或者- (void)removeObserver:forKeyPath:context:来移除观察者,需要注意的是如果移除了一个没有观察过的属性,程序会抛出异常。也就是说如果之前观察的是"property1",而在移除的时候keyPath参数写的是"property2",这是就会有异常被抛出。可以使用@try/@catch来防止崩溃。

    @try {
        [object removeObserver:observer forKeyPath:@"keyPath")];
    }
    @catch (NSException * __unused exception) {}
    
    手动通知

    默认情况下通知会被自动发送,但有的时候我们希望可以手动的控制它。这时候需要在被观察对象的类里面重写+ (BOOL)automaticallyNotifiesObserversForKey:方法。例如被观察对象有一个属性叫"bankCodeEn",我们希望这个属性被修改时的通知由我们手动控制,就需要在被观察对象的类文件里面这样写:
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    // 如果属性为bankCodeEn则关闭自动发送通知
    BOOL automatic = YES;
    if ([key isEqualToString:@"bankCodeEn"]) {
    automatic = NO;
    } else {
    // 对于对象中其它没有处理的属性,我们需要调用[super automaticallyNotifiesObserversForKey:key],以避免无意中修改了父类的属性的处理方式
    automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
    }

    然后再对"bankCodeEn"属性的setter方法做如下处理:
    - (void)setBankCodeEn:(NSString *)bankCodeEn{
    //当两次赋予的值完全相等时,没有必要再发送通知。这个if的条件语句可以根据实际需要自行修改,或者干脆不写。
    if (_bankCodeEn != bankCodeEn) {
    [self willChangeValueForKey:@"bankCodeEn"];
    _bankCodeEn = bankCodeEn;
    [self didChangeValueForKey:@"bankCodeEn"];
    }
    }
    注意 willChangeValueForKey:和didChangeValueForKey:方法在默认自动发送通知的情况下是由系统自动调用的,在手动通知时需要我们自己来调用,并且不应该重写这两个方法。

    注册依赖建

    有时一个属性的改变需要依赖其他的属性,比如一个叫"fullName"的属性,这个属性依赖于"firstName"和"lastName"。

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

    这种情况下如果firstName发生了变化,fullName的值自然也会改变,但是由于没有直接使用setter方法设置fullName,所以如果不做特殊设置的话KVO是不会发送通知的。

    这种情况就需要使用注册依赖建来解决。
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
    keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"firstName", @"lastName"]];
    }
    return keyPaths;
    }

    这样不论firstName,lastName,fullName中的哪个值放生了变化,监听fullName的KVO都会被触发。还可以使用这个方法来达到同样的目的。
    + (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"firstName", @"lastName", nil];
    }
    这个方法的使用规则是+ (NSSet *)keyPathsForValuesAffecting + 属性名(注意属性名首字母大写)。

    属性类型为集合的监听

    对于集合的KVO,我们需要了解的一点是,KVO旨在观察关系(relationship)而不是集合。对于不可变集合属性,我们更多的是把它当成一个整体来监听,而无法去监听集合中的某个元素的变化;对于可变集合属性,实际上也是当成一个整体,去监听它整体的变化,如添加、删除和替换元素。

    例如一个叫arr的NSArray类型属性,我们可以使用集合代理对象(collection proxy object)来处理集合相关的操作。有下面的几个代理方法需要实现

    -countOf<Key>
    
    // 以下两者二选一
    -objectIn<Key>AtIndex:
    -<key>AtIndexes:
    
    // 可选(增强性能)
    -get<Key>:range:
    

    具体实现如下
    - (NSUInteger)countOfArr{
    return [_arr count];
    }
    - (id)objectInArrAtIndex:(NSUInteger)index {
    return [_arr objectAtIndex:index];
    }

    当我们使用对象的arr属性时,通过[object valueForKey:@"arr"]来获取该属性,这个方法返回的代理数组对象支持所有正常的NSArray调用。换句话说,调用者并不知道返回的是一个真正的NSArray,还是一个代理的数组。

    对于可变数组的操作

    对于可变数组的代理对象,我们需要实现以下几个方法:

    // 至少实现一个插入方法和一个删除方法
    -insertObject:in<Key>AtIndex:
    -removeObjectFrom<Key>AtIndex:
    -insert<Key>:atIndexes:
    -remove<Key>AtIndexes:
    
    // 可选(增强性能)以下方法二选一
    -replaceObjectIn<Key>AtIndex:withObject:
    -replace<Key>AtIndexes:with<Key>:
    

    实现如下

    - (NSUInteger)countOfArr{
      return [_arr count];
    }
    
    - (id)objectInArrAtIndex:(NSUInteger)index{
      return [_arr objectAtIndex:index];
    }
    
    - (void)insertObject:(id)object inArrAtIndex:(NSUInteger)index{
      [_arr insertObject:object atIndex:index];
    }
    
    - (void)removeObjectFromArrAtIndex:(NSUInteger)index{
      [_arr removeObjectAtIndex:index];
    }
    
    - (void)replaceObjectInArrAtIndex:(NSUInteger)index withObject:(id)object{
      [_arr replaceObjectAtIndex:index withObject:object];
    }
    

    方法实现后,需要使用[object mutableArrayValueForKey:@"arr"]来访问arr属性才能或取到代理数组。在使用时访问真正数组对象和集合代理对象差别还是很大的。

    BankObject *bankInstance = [[BankObject alloc] init];
    PersonObject *personInstance = [[PersonObject alloc] init];
    [bankInstance addObserver:personInstance forKeyPath:@"departments" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
    bankInstance.departments = [[NSMutableArray alloc] init];
    [bankInstance.departments addObject:@"departments"];
    

    这段代码BankObject是被观察对象,PersonObject是观察者对象。BankObject类里面有一个叫departments的可变数组属性。

    这段代码只会触发一次KVO,也就是只有在给departments赋予一个初始化数组的时候KVO被触发,在给数组添加内容的时候并没有触发。

    BankObject *bankInstance = [[BankObject alloc] init];
    PersonObject *personInstance = [[PersonObject alloc] init];
    [bankInstance addObserver:personInstance forKeyPath:@"departments" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
    bankInstance.departments = [[NSMutableArray alloc] init];
    NSMutableArray *departments = [bankInstance mutableArrayValueForKey:@"departments"];
    [departments insertObject:@"departments 0" atIndex:0];
    

    使用集合代理对象的方式会触发两次KVO,在给数组插入(删除,替换)数据的时候KVO也会被触发。

    监听信息

    对于被观察的对象,可以使用observationInfo属性获取都有哪些观察者观察了哪些属性。
    id info = bankInstance.observationInfo;
    NSLog(@"%@", [info description]);
    如果像这样获取了一个被观察对象的info然后打印出来,会看到这样的结果。

    <NSKeyValueObservationInfo 0x7fdc236a19d0> (
    <NSKeyValueObservance 0x7fdc236a17a0: Observer: 0x7fdc2369e5e0, Key path: bankCodeEn, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x7fdc236a15c0>
    <NSKeyValueObservance 0x7fdc236a1960: Observer: 0x7fdc2369e5e0, Key path: accountBalance, Options: <New: NO, Old: YES, Prior: NO> Context: 0x0, Property: 0x7fdc236a1880>
    )
    

    我们可以看到observationInfo指针实际上是指向一个NSKeyValueObservationInfo对象,它包含了指定对象上的所有的监听信息。而每条监听信息而是封装在一个NSKeyValueObservance对象中,从上面可以看到,这个对象中包含消息的观察者、被监听的属性、添加观察者时所设置的一些选项、上下文信息等。

    其他的一些小tips

    1、如果重复添加注册观察者的方法(addObserver),比如像这样完全一样的两句代码重复两次,那么通知也就会发送两次,系统不会检查也不会替换覆盖。

    [bank addObserver:per1 forKeyPath:NSStringFromSelector(@selector(departments)) options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    [bank addObserver:per1 forKeyPath:NSStringFromSelector(@selector(departments)) options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    

    2、因为keyPath是字符串类型,这就导致写错的情况很容易发生,keyPath写错严重的话就会导致程序崩溃。所以为了避免这种情况,可以将@"property"替换成NSStringFromSelector(@selector(property)),这样写首先在敲属性名的时候会有提示,而且在你把属性名敲错的时候由于xcode没有在对应的类里面找到那个被你写错的属性,就会报出警告。像这样:

    [bank addObserver:per1 forKeyPath:NSStringFromSelector(@selector(departments)) options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    

    3、关于context可以这样设置,一个静态变量存着它自己的指针。这意味着它自己什么也没有。

    static void * XXContext = &XXContext;
    
    关于KVO的实现机制

    KVO使用了OC的runtime来实现,在第一次观察一个对象时,runtime会创建一个继承自被观察对象的类的子类,这个子类重写了被观察属性的setter方法,然后将这个对象的is a指针指向了这个新建的类。也就是说其实这个被观察的对象在程序运行时所属的类已经不是之前我们自己写的那个类了,而是系统创建的子类。

    参考的文章:
    Foundation: NSKeyValueObserving(KVO)
    iOS KVO(键值观察) 总览
    Key-Value Observing

    相关文章

      网友评论

        本文标题:KVO in iOS

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