浅谈kvo

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/szk972092933/article/details/78650220

关于kvo即Key-Value Observing ,下面记下读官方文档https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html的个人总结,一方面希望能够加深对kvo的了解的,另一方面能够和大家一起讨论,如有错误,请留言指正,万分感谢。

一、什么是kvo

kvo是一个对象用来观察另一个对象(可以是自己)属性的变化,并作出处理的一种机制。当被观察对象的属性变化时,系统会发出一个通知,来通知观察对象。kvo实现机制的前提是对象遵守kvc非正式协议,所有继承于NSObject的对象,并且按一般命名规则定义属性的,都遵守kvc机制。

假设有一个Person对象,他有一个Account属性。他需要当账户发生变动时接收到一个通知,首先account要注册person为其观察者addObserver:forKeyPath: options:context:,person对象为了收到通知,需实现observeValueForKeyPath:ofObject:change:context:方法,如果不想监听账户的变化了,那么需要在person 对象dealloc调用之前,调用removeObserver:forKeyPath: 移除对账户特定属性的监听.

Options

addObserver:forKeyPath: options:context:,参数options 传入按位或运算的常量,不但影响到通知的内容,还影响到通知产生的方式,通过NSKeyValueObservingOptionOld.来获取属性变化之前的值,通过NSKeyValueObservingOptionNew获取属性变化之后的值,通过或运算 | 来同时获取变化之前和变化之后的值即传入NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew。也可传入NSKeyValueObservingOptionInitial来让被观察对象在addObserver:forKeyPath: options:context:return之前发出一个一次通知。 可以用来在observeValueForKeyPath:ofObject:change:context:初始化观察者对象属性的值。options的值传入NSKeyValueObservingOptionPrior可以在被观察对象属性的值发生变化之前,由被观察对象发出一个通知.

Context 可以包含任意的数据,并被传参到OberserveValueForKeyPath:ofObject:change:context:方法中,你可以传入NULL,但是当观察对象的父类对象同时对该KeyPath注册了观察者的时候就会产生问题,因此安全的做法是可以使用context来区分观察者是子类还是父类。你可以为所有的类定义一个context,而用keyPath来区分观察的属性,也可以为所有要观察的对象的属性来定义不同的context,然后通过不同的context来区分观察的属性。记住在找不到对应的属性时一定要调用【super observeValueForKeyPath:ofObject:change:context:]方法,因为有可能是父类的监听

举例:

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
 
 
     
- (void)registerAsObserverForAccount:(Account*)account {
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}
需要注意的是这个方法中,对于观察者,被观察者,context都是弱引用。
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}
change Dictionary key NSKeyValueChangeKindKey包含了变化的类型,如果被观察对象的值发生了变化,那么对NSKeyValueChangeKindKey取值会获得 NSKeyValueChangeSetting.值 依赖于注册观察者时options传入的参数, NSKeyValueChangeOldKey 和 NSKeyValueChangeNewKey,分别返回变化之前的值和改变之后的值,如果被观察的对象属性是一个对象,那么直接返回该对象的值,如果是标量或者是c的结构体,那么返回NSNumber对象或者,NSValue对象。
如果被观察的对象的属性是一个to-many 关系,那么NSKeyValueChangeKindKey通过返回NSKeyValueChangeInsertionNSKeyValueChangeRemoval, or NSKeyValueChangeReplacement来表示这个对象属性所做改变的方式是插入,删除,或者是替换,而NSKeyValueChangeIndexesKey则返回一个包含所改变的索引的NSSet对象,NSKyeValueCHangeOldKey和NSKeyValueChangeNewKey则返回改变之前的集合对象和该变之后的集合对象。

当你不需要监听被观察对象属性变化的时候一定要记得调用 removeObserver: forKeyPath: context方法,并且在调用这个方法之前,一定要确认 已经注册了观察者,否者会抛出NSRangeException异常,当然可以在try-catch block里来调用捕获异常。观察对象并不会自动移除观察模式,即使它已经被dealloc掉了,因此它会继续接受被观察者对象发出的通知,而对一个被销毁的对象接受通知会导致内存异常。协议并没有提供是否是观察者或者是被观察者的方法,因此需要合理的组织代码,一般在初始化或者viewDidLoad里注册观察者,在dealloc里移除观察者。

- (void)unregisterAsObserverForAccount:(Account*)account {
    [account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
    [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];
}

	

二、如何手动控制kvo的过程

那么什么样的类的属性是遵守kvo的呢?1、首先这个类的属性必须是遵守kvc的2这个类为这个属性的变化发送kvo通知3、所依赖的key要被正确注册

什么情况下改变属性会发送kvo通知呢?1.使用通用的set 方法,2.使用kvc中的setValueForKey:和setValueForKeyPath:都会使类自动发送kvo通知。

手动发送kvo通知:在有些情况下,你想控制发送通知的过程,来减少不必要的通知,或者是将一组数据的变化通知,改成一个通知。想控制通知的发送过程需要重写automaticallyNotifiesObserversForKey:类方法。对于手动管理的通知的属性返回NO, 对于不手动控制通知过程的属性,用super调用该方法。举例如下:

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

手动控制通知的发送需要调用willChangeValueForKey:和didChangeValueForKey:方法 

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
如果是多个属性,那么需要进行嵌套:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}
对于一个有序的to-many关系的属性,当它变化时,你不但要指出它的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"];
}

三、关于有依赖关系的Keys kvo的处理

在许多情况下一个对象的属性值,依赖于其他对象的多个属性的值,因此当依赖对象的一个属性值发生变化时,需要收到通知。例如一个人的名字包含姓和名,获取全名的getter方法如下

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
当一个对象监听fullName的变化,它需要在firstName改变和LastName属性改变的时候都接收到通知,那么可以有两种方法来注册这种依赖关系1.重写
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key方法

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
2.实现方法,keyPahtsForValuesAffecting<Key>

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
但是对于集合类的属性这两个方法都不行,假设有一个Department对象,有一个 to-many relationship的属性employees(对于Employee对象),而Employee有一个salary属性。而Department对象有一个属性totalSalary,依赖于emplyees中所有Employee对象的salary属性。那么可以Department对象注册所有employees中Employee对象属性salary的监听,并负责移除监听

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}


猜你喜欢

转载自blog.csdn.net/szk972092933/article/details/78650220
KVO