键值观察是一种机制,允许将其他对象的指定属性的更改通知对象。
重要:为了理解键值观察,您必须首先理解键值编码。
键值观察提供了一种机制,允许将其他对象的特定属性的更改通知对象。它对于应用程序中模型层和控制器层之间的通信特别有用。(在OS X中,控制器层绑定技术严重依赖于键值观察。)控制器对象通常观察模型对象的属性,视图对象通过控制器观察模型对象的属性。此外,模型对象可以观察其他模型对象(通常用于确定依赖值何时发生变化),甚至可以观察自身(再次用于确定依赖值何时发生变化)。
您可以观察属性,包括简单属性、一对一关系和多对多关系。许多关系的观察者被告知所做变更的类型以及变更中涉及到的对象。
一个简单的示例演示了KVO如何在应用程序中发挥作用。假设Person对象与Account对象交互,表示该对象在银行的储蓄帐户。Person的实例可能需要知道Account实例的某些方面何时发生变化,比如余额或利率。
如果这些属性是Account的公共属性,那么用户可以定期轮询帐户以发现更改,但这当然是低效的,而且通常不切实际。更好的方法是使用KVO,它类似于在发生更改时接收中断的人。
要使用KVO,首先必须确保所观察的对象(本例中的帐户)符合KVO。通常,如果您的对象继承自NSObject,并且您以通常的方式创建属性,那么您的对象及其属性将自动与KVO兼容。手动实现遵从性也是可能的。KVO遵从性描述了自动和手动键值观察之间的区别,以及如何实现这两者。
接下来,您必须注册您的观察者实例(Person)和观察到的实例(Account)。Person向帐户发送一个addObserver:forKeyPath:options:context: message,每个观察到的密钥路径发送一次,并将其命名为observer。
为了从帐户接收更改通知,Person实现了所有观察者都需要的observeValueForKeyPath:ofObject:change:context: method。一旦注册的密钥路径发生更改,该帐户将向该人发送此消息。然后,该人可以根据变更通知采取适当的行动。
最后,当不再需要通知时(至少在解除分配之前),Person实例必须通过发送消息removeObserver:forKeyPath:到帐户来注销注册。
注册键值观察描述了注册、接收和注销键值观察通知的完整生命周期。
KVO的主要好处是,您不必实现自己的方案来在每次属性更改时发送通知。它定义良好的基础架构具有框架级支持,这使得它易于采用——通常您不需要向项目中添加任何代码。此外,基础设施已经具备了完整的功能,这使得支持单个属性的多个观察者和依赖值变得很容易。
注册依赖键说明如何指定一个键的值依赖于另一个键的值。
与使用NSNotificationCenter的通知不同,没有为所有观察者提供更改通知的中心对象。相反,当发生更改时,通知将直接发送到观察对象。NSObject提供了键值观察的基本实现,您应该很少需要覆盖这些方法。
注册键值观察
您必须执行以下步骤,以使对象能够接收符合kvo的属性的键值观察通知:
1.使用addObserver:forKeyPath:options:context:方法向被观察对象注册观察者。
2.在观察者内部实现observeValueForKeyPath:ofObject:change:context:接受变更通知消息。
3.使用removeObserver:forKeyPath方法注销观察者的注册:当它不再应该接收消息时。至少,在观察者从内存中释放之前调用这个方法。
重要:并不是所有的类都与kvo兼容。通过遵循KVO遵从性中描述的步骤,您可以确保您自己的类符合KVO。通常,苹果提供的框架中的属性只有在这样的文档中才符合kvo。
注册为观察者
观察对象首先通过发送addObserver:forKeyPath:options:context: message向观察对象注册自己,并将自己作为观察者和要观察的属性的键路径进行传递。观察者还指定一个选项参数和一个上下文指针来管理通知的各个方面
选项
选项参数(以位或选项常量的形式指定)影响通知中提供的更改字典的内容和生成通知的方式。
通过指定选项NSKeyValueObservingOptionOld,您可以选择从更改之前接收被观察属性的值。使用选项NSKeyValueObservingOptionNew请求属性的新值。通过按位或这些选项中的一个,可以同时接收新值和旧值。
指示被观察对象使用NSKeyValueObservingOptionInitial选项发送一个即时更改通知(在addObserver:forKeyPath:options:context: returns之前)。您可以使用此附加的一次性通知在观察者中建立属性的初始值。
通过包含选项NSKeyValueObservingOptionPrior,可以指示被观察对象在属性更改之前(除了通常的更改之后的通知之外)发送通知。更改字典通过包含NSNumber包装YES值的键NSKeyValueChangeNotificationIsPriorKey来表示更改前通知。否则该键不存在。当观察者自己的KVO遵从性要求它为依赖于被观察属性的属性之一调用-willChange…方法时,您可以使用prechange通知。通常的更改后通知来得太晚,无法及时调用willChange。
上下文
消息中addObserver:forKeyPath:options:context: message中的上下文指针包含任意数据,这些数据将在相应的更改通知中传递回观察者。您可以指定NULL并完全依赖于键路径字符串来确定更改通知的来源,但是这种方法可能会为其超类由于不同原因也在观察相同键路径的对象带来问题。
一种更安全、更可扩展的方法是使用上下文来确保接收到的通知是针对观察者而不是超类的。
类中唯一命名的静态变量的地址是一个很好的上下文。在超类或子类中以类似方式选择的上下文不太可能重叠。您可以为整个类选择一个上下文,并依赖于通知消息中的键路径字符串来确定更改了什么。或者,您可以为每个观察到的键路径创建不同的上下文,这完全避免了字符串比较的需要,从而产生更高效的通知解析。清单1显示了以这种方式选择的balance和interestRate属性的示例上下文。
清单1创建上下文指针
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
清单2中的示例演示了Person实例如何使用给定的上下文指针将自己注册为Account实例的balance和interestRate属性的观察者。
清单2将检查器注册为balance和interestRate属性的观察者
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
注意:观察addObserver:forKeyPath:options:context:方法的键值不维护对观察对象、被观察对象或上下文的强引用。您应该确保在必要时维护对观察对象、被观察对象和上下文的强引用。
接收更改通知
当对象的被观察属性的值发生变化时,观察者接收到一个observeValueForKeyPath:ofObject:change:context: message。所有观察者都必须实现这个方法。
观察对象提供触发通知的键路径、本身作为相关对象、包含更改细节的字典以及在为该键路径注册观察者时提供的上下文指针。
change dictionary条目NSKeyValueChangeKindKey提供关于发生的更改类型的信息。如果所观察对象的值发生了更改,NSKeyValueChangeKindKey条目将返回NSKeyValueChangeSetting。根据观察者注册时指定的选项,更改字典中的NSKeyValueChangeOldKey和NSKeyValueChangeNewKey条目包含更改之前和之后的属性值。如果属性是对象,则直接提供值。如果属性是标量或C结构,则将值包装在NSValue对象中(与键值编码一样)。
如果观察到的属性是一对多关系,NSKeyValueChangeKindKey条目还指示关系中的对象是否分别被插入、删除或替换为返回nskeyvaluechangeinsert、NSKeyValueChangeRemoval或NSKeyValueChangeReplacement。
NSKeyValueChangeIndexesKey的更改字典条目是一个NSIndexSet对象,它指定了更改关系中的索引。如果NSKeyValueObservingOptionNew或NSKeyValueObservingOptionOld在观察者注册时被指定为选项,那么变更字典中的NSKeyValueChangeOldKey和NSKeyValueChangeNewKey条目就是数组,数组中包含了在变更前后相关对象的值。
清单3中的示例显示了Person观察者的observeValueForKeyPath:ofObject:change:context: implementation,它记录了balance和interestRate属性的新旧值,如清单2中所注册的那样。
清单3实现了observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
//做一些Balance的事情……
} else if (context == PersonAccountInterestRateContext) {
//做一些interest rate…的事情
} else {
// 任何未识别的上下文都必须属于super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
如果在注册观察者时指定了空上下文,则将通知的键路径与正在观察的键路径进行比较,以确定发生了什么更改。如果对所有观察到的键路径使用单一上下文,则首先根据通知的上下文对其进行测试,并找到匹配项,然后使用键路径字符串比较来确定具体更改了什么。如果您为每个键路径提供了惟一的上下文(如本文所示),那么一系列简单的指针比较将同时告诉您该通知是否针对该观察者,如果是,则告诉您哪些键路径发生了更改。
在任何情况下,当观察者不识别上下文(或者在简单的情况下,任何关键路径)时,它应该总是调用超类的observeValueForKeyPath:ofObject:change:context:的实现,因为这意味着超类也注册了通知。
注意:如果通知传播到类层次结构的顶部,NSObject将抛出nsinternalin一致性异常,因为这是一个编程错误:子类未能使用它注册的通知。
作为观察者删除一个对象
通过向观察对象发送removeObserver:forKeyPath:context: message,指定观察对象、键路径和上下文,可以删除键值观察者。清单4中的示例显示Person将自己作为balance和interestRate的观察者删除。
清单4将检查器作为balance和interestRate的观察者删除
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
接收到removeObserver:forKeyPath:context: message后,观察对象将不再接收指定键路径和对象的observeValueForKeyPath:ofObject:change:context: messages。
在移除观察者时,请记住以下几点:
1.如果没有注册为观察者,请求以观察者的身份被删除会导致NSRangeException异常。您可以调用removeObserver:forKeyPath:context:对addObserver:forKeyPath:options:context:的对应调用一次,或者如果在您的应用程序中不可行,则将removeObserver:forKeyPath:context: call放在try/catch块中,以处理潜在的异常。
2.解除分配时,观察者不会自动删除自身。被观察对象继续发送通知,而不注意观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。因此,您要确保观察者在从内存中消失之前删除自己。
3.协议没有提供询问对象是否是观察者或被观察者的方法。构造代码以避免发布相关错误。典型的模式是在观察者的初始化过程中注册为观察者(例如在init或viewDidLoad中),在解除分配过程中取消注册(通常在dealloc中),确保正确地对和有序地添加和删除消息,并且在观察者从内存中释放之前取消注册。
为了被认为符合特定属性的kvo,类必须确保以下内容:
1.类必须是符合属性的键值编码,如在确保KVC遵从性中指定的那样。
KVO支持与KVC相同的数据类型,包括Objective-C对象以及标量和结构支持中列出的标量和结构。
2.该类为属性发出KVO更改通知。
3.适当地注册依赖键。
有两种技术可以确保发出更改通知。NSObject提供了自动支持,默认情况下,对符合键值编码的类的所有属性都可用。通常,如果您遵循标准的Cocoa编码和命名约定,您可以使用自动更改通知—您不需要编写任何额外的代码。
手动更改通知提供了对何时发出通知的额外控制,并且需要额外的编码。您可以通过实现类方法automaticallyNotifiesObserversForKey:来控制子类属性的自动通知。
自动更改通知
NSObject提供了自动键值更改通知的基本实现。自动键值更改通知通知观察者使用符合键值的访问器以及键值编码方法所做的更改。由mutableArrayValueForKey:返回的集合代理对象也支持自动通知。
清单1所示的示例会将更改通知属性名的任何观察者。
清单1发出KVO更改通知的方法调用示例
// 调用访问器方法。
[account setName:@"Savings"];
// 使用setValue: forKey:。
[account setValue:@"Savings" forKey:@"name"];
// 使用密钥路径,其中‘account’是‘document’的kvc兼容属性。
[document setValue:@"Savings" forKeyPath:@"account.name"];
//使用mutableArrayValueForKey:检索关系代理对象。
Transaction *newTransaction = <#Create a new transaction for the account#><#为帐户#>创建一个新事务;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
手动更改通知
在某些情况下,您可能希望控制通知流程,例如,最小化由于应用程序特定原因而不必要的触发通知,或者将一些更改分组到单个通知中。手动更改通知提供了这样做的方法。
手动通知和自动通知并不相互排斥。除了已经存在的自动通知之外,您还可以自由地发出手动通知。更典型的是,您可能希望完全控制特定属性的通知。在本例中,您覆盖了automaticallyNotifiesObserversForKey:的NSObject实现。对于您希望阻止其自动通知的属性,automaticallyNotifiesObserversForKey:的子类实现应该返回NO。子类实现应该为任何未识别的键调用super。清单2中的示例支持balance属性的手动通知,允许超类确定所有其他键的通知。
例二automaticallyNotifiesObserversForKey:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要实现手动观察者通知,您可以在更改值之前调用willChangeValueForKey:,在更改值之后调用didChangeValueForKey:。清单3中的示例实现了balance属性的手动通知。
清单3实现手动通知的示例访问器方法
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
您可以通过首先检查值是否已更改来最小化发送不必要的通知。清单4中的示例测试balance的值,并且只在余额发生更改时提供通知。
清单4在提供通知之前测试更改的值
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
如果单个操作导致多个键发生更改,则必须嵌套更改通知,如清单5所示。
清单5嵌套多个键的更改通知
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
对于有序到多关系,不仅必须指定更改的键,还必须指定更改的类型和涉及的对象的索引。更改的类型是NSKeyValueChange,它指定nskeyvaluechangeinsert、NSKeyValueChangeRemoval或NSKeyValueChangeReplacement。受影响对象的索引作为NSIndexSet对象传递。
清单6中的代码片段演示了如何包装to-many关系事务中删除的对象。
清单6在一对多关系中实现手动观察者通知
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
//删除指定索引处的事务对象。
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}
注册相关的键
在许多情况下,一个属性的值取决于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生了变化,那么派生属性的值也应该标记以供更改。如何确保为这些依赖属性发布观察通知的键值取决于关系的基数。
一个关系
要自动触发一对一关系的通知,您应该覆盖keyPathsForValuesAffectingValueForKey:或者实现一个合适的方法,该方法遵循它为注册依赖键定义的模式。
例如,一个人的全名取决于他的姓和名。返回全名的方法可以这样写:
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
观察fullName属性的应用程序必须在firstName或lastName属性更改时得到通知,因为它们会影响属性的值。
一种解决方案是覆盖keyPathsForValuesAffectingValueForKey:指定person的fullName属性依赖于lastName和firstName属性。清单1显示了这种依赖关系的示例实现:
清单1 keyPathsForValuesAffectingValueForKey的示例实现:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
您的覆盖通常应该调用super并返回一个集合,该集合中包含由此而产生的任何成员(以便不干扰超类中该方法的覆盖)。
您还可以通过实现一个类方法来实现相同的结果,该类方法遵循影响的keypathsforvaluesnaming约定,其中是依赖于值的属性的名称(第一个大写字母)。使用此模式,可以将清单1中的代码重写为名为keyPathsForValuesAffectingFullName的类方法,如清单2所示。
清单2影响命名约定的keypathsforvaluesaffect的示例实现
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
当您使用类别向现有类添加计算属性时,您不能覆盖keyPathsForValuesAffectingValueForKey:方法,因为您不应该覆盖类别中的方法。在这种情况下,实现一个匹配的keypathsforvaluesaffect 类方法,以利用这种机制。
注意:您不能通过实现keyPathsForValuesAffectingValueForKey:来设置对多个关系的依赖关系。相反,您必须观察to-many集合中每个对象的适当属性,并通过自己更新依赖键来响应其值的更改。
很多关系
方法不支持包含对多关系的键路径。例如,假设您有一个部门对象,该对象与一个雇员(雇员)具有多对多的关系,并且雇员具有一个salary属性。您可能希望Department对象具有totalSalary属性,该属性依赖于关系中所有员工的工资。例如,keypathsforvaluesaffect total salary和return employees就不能这样做。薪水是关键。
两种情况都有两种可能的解决方案:
您可以使用键值观察来将父节点(在本例中为Department)注册为所有子节点(在本例中为employee)的相关属性的观察者。您必须以观察者的身份添加和删除父对象,因为在关系中添加和删除子对象。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
//处理其他观察和/或调用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;
}
如果使用Core Data,可以将父对象注册到应用程序的通知中心,作为其托管对象上下文的观察者。父节点应该以类似于观察键值的方式响应子节点发布的相关更改通知。
观察实现细节的键值
自动键值观察是使用isa- swizzle技术实现的。
顾名思义,isa指针指向维护分派表的对象的类。这个分派表本质上包含指向类实现的方法和其他数据的指针。
当一个观察者注册了一个对象的属性时,被观察对象的isa指针被修改,指向一个中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。
永远不要依赖isa指针来确定类的成员。相反,您应该使用类方法来确定对象实例的类