Key-Value Observing

来源:互联网 发布:淘宝详情页思路 编辑:程序博客网 时间:2024/05/29 09:42

介绍

Key-Value Observing 简称KVO,中文名键值观察。如果学过设计模式,那么它其实就是借助于KVC实现的观察者模式。一个观察者A观察B的属性,当B的属性引起变化时,通知A做出相应的决策。下面简略介绍了一个PersonObject观察自己的银行中的账户余额属性,当账户余额发生变化时通知观察者做出相应决策

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

PS: 在学习KVO之前,一定要先学习KVC。

具体实现

为了实现KVO,以下三个步骤缺一不可:

  • 被观察类的被观察属性必须满足KVO-Compliance
  • 将观察者注册到被观察对象中,给被观察者发送addObserver:forKeyPath:options:context:消息
  • 观察者必须实现observerValueForKeyPath:ofObject:change:context:方法。

PS:并不是所有类的所有属性都满足KVO-Compliance的,这个会在后面KVO Compliance这一节详细讨论。一般情况下,苹果提供的框架满足KVO-Compliance。

注册观察者

- (void)registerAsObserver {    /*     Register 'inspector' to receive change notifications for the "openingBalance" property of     the 'account' object and specify that both the old and new values of "openingBalance"     should be provided in the observe… method.     */    [account addObserver:inspector             forKeyPath:@"openingBalance"                 options:(NSKeyValueObservingOptionNew |                            NSKeyValueObservingOptionOld)                 context:NULL];}

options 中的参数和observerValueForKeyPath:ofObject:change:context:中的change字典有关,如果提供NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld,则change字典中会提供属性值变化前和变化后的值。context参数可以是C指针也可以是对象的引用。在observerValueForKeyPath:ofObject:change:context:调用的时候context会可能被用到,主要是用来标记当前通知的上下文或者提供一些其他信息数据,几乎很少用一般为NULL。

接收变化通知

当被观察属性变化时,会调用观察者的如下方法:

- (void)observeValueForKeyPath:(NSString *)keyPath                      ofObject:(id)object                        change:(NSDictionary *)change                       context:(void *)context {    if ([keyPath isEqual:@"openingBalance"]) {        [openingBalanceInspectorField setObjectValue:            [change objectForKey:NSKeyValueChangeNewKey]];    }    /*     Be sure to call the superclass's implementation *if it implements it*.     NSObject does not implement the method.     */    [super observeValueForKeyPath:keyPath                         ofObject:object                           change:change                           context:context];}

其中change参数是一个字典,包含了注册时候提供的登记的键,例如NSKeyValueChangeOldKey, NSKeyValueChangeNewKey。如果被观察属性还是to-many relationship的类型,则NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement表示当其内容有新增,移除,替换的变动信息。

移除观察者

- (void)unregisterForChangeNotification {    [observedObject removeObserver:inspector forKeyPath:@"openingBalance"];}

KVO Compliance

前面提到被观察类的被观察属性必须满足KVO-Compliance,这里介绍满足KVO-Compliance的三个要求:

  • 类的属性必须满足KVC Compliance。
  • 当属性变化时,类能够为之发出通知。
  • 依赖键被合适地注册

第一个条件在KVC那篇文章中有介绍。
第二个条件有两种实现方法。

第一种是NSObject自动支持,这种方式对类中的所有满足KVC Compliance的属性适用,如果你按照Cocoa的编程习惯,适用这种方式,将不需要写任何其他代码,就能实现自动通知。

以下语句都能够触发KVO自动通知

// Call the accessor method.[account setName:@"Savings"];// Use setValue:forKey:.[account setValue:@"Savings" forKey:@"name"];// Use a key path, where 'account' is a kvc-compliant property of 'document'.[document setValue:@"Savings" forKeyPath:@"account.name"];// Use mutableArrayValueForKey: to retrieve a relationship proxy object.Transaction *newTransaction = <#Create a new transaction for the account#>;NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];[transactions addObject:newTransaction];

第二种是手动支持,手动支持有一个好处就是,可以在通知发出之前做一些额外的工作。其麻烦的地方就是需要额外的编码工作。你可以重写被观察者属性的所在类的类方法automaticallyNotifiesObserversForKey:来实现。

+ (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 {    [self willChangeValueForKey:@"openingBalance"];    _openingBalance = theBalance;    [self didChangeValueForKey:@"openingBalance"];}

如果是to-many relationship的属性,对其内容具体变化进行操作

- (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"];}

注册依赖键

在实际开发中,往往一个属性是由其他属性一起决定的。例如一个人的fullName是由firstName和lastName决定的。或者说一个集合类型的属性,由其内容元素变化引起改变。这些属性与属性之间,集合与元素之间都存在依赖。因此这种依赖也是可以用KVO来进行通知变化的,以保证数据一致性。

属性与属性之间的改变我们通常称之为to-one relationship。其实现方式需要重写被观察者的keyPathsForValuesAffectingValueForKey:方法。就比如说,fullName的定义如下:

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

则为了保持fullName与 firstName和lastName的一致性,需要进行如下操作:

+ (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>是唯一表示属性的key.这个fullName这个案例中可以这么实现:

+ (NSSet *)keyPathsForValuesAffectingFullName {    return [NSSet setWithObjects:@"lastName", @"firstName", nil];}

这个方法效果与上面那个一样的。但是千万别再to-many relationship中使用。下面会介绍如何确定to-many relationship的依赖通知

接下来假设如下场景,一个部门Department有很多员工Employee,每个员工都有salary属性,而部门的totalSalary依赖于每个员工的salary。当员工的salary发生变化,部门的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:@"employees.@sum.salary"]];}- (void)setTotalSalary:(NSNumber *)newTotalSalary {    if (totalSalary != newTotalSalary) {        [self willChangeValueForKey:@"totalSalary"];        _totalSalary = newTotalSalary;        [self didChangeValueForKey:@"totalSalary"];    }}- (NSNumber *)totalSalary {    return _totalSalary;}

KVO的实现原理

自动KVO的实现是依赖于一种叫做isa-swizzling的技术。由于每个对象都有isa指针指向它的类,而类结构拥有一个分派表,保存了每个方法的实际地址。当一个观察者被注册观察某个属性的时候,被观察对象的isa指针被修改,指向它的直接类,而不是它真实类,因此isa指针破坏了被观察对象和它实际的类的映射。其具体解释见转载KVO实现原理

0 0
原创粉丝点击