KVO
简介
KVO(Key-value observing)
提供了一种机制,允许对象去监听另外特定对象的属性更改。这对于App中的modal和controller
层和控制器层通信特别有用。controller
对象通常监听model对象属性,view对象通过controller监听model对象属性。而,model对象通常会监听其他model对象
可以观察属性,包括简单属性、一对一关系和一对多关系。一对多关系的观察者被告知所做的更改类型,以及涉及哪些对象。
要使用KVO首先确保 要观察的对象支持KVO。通常,如果对象继承NSObject并以通常的方式创建的属性,那么你的对象以及其属性会自动符合KVO标准。当然也可以手动实现来符合规格。KVO Compliance描述了再自动和手动KVO之间的区别,以及如何实现两者。
KVO最主要的好处是:
- 我们无需执行自己的方案 接口在每次属性更改时,发送通知。
- 并且因为其架构具有框架级别的支持,使其很方便的使用,而且通常不需要在项目中添加任何代码;
- 因为KVO架构的完整性,这使对一个属性的多观察者变得很容易
与NSNotification
不同的是,其并没有center object来为所有观察者提供更改通知。而是,在进行更改时将通知直接发送到观察对象。NSObject
提供了KVO的基本实现,因此几乎不需要重写这些方法。Key-Value Observing Implementation Details提供了KVO实现的细节
注册KVO
为了使一个对象能有接收符合KVO属性的键值观察通知,必须执行以下步骤:
- 使用
addObserver:forKeyPath:options:context:.
向观察者对象注册观察者 - 在观察者内部实现
observeValueForKeyPath:ofObject:change:context:
以接受更改通知消息 - 当观察者不再应接收消息时 使用
removeObserve:forKeyPath:
注销观察者。(至少在内存释放之前调用此方法)
注册观察者
addObserver:forKeyPath:options:context:
option参数
既会影响通知中字典的内容,又会影响生成通知的方式.
- 可以通过指定选项
NSKeyValueObservingOptionOld
选择从更改之前接收观察到的属性的值。使用NSKeyValueObservingOptionNew
来请求属性的新值。而将其OR之后,可以同时收到新值和旧值 - 通过指定
NSKeyValueObservingOptionInitial
发送立即更改通知,可以使用此附加一次性通知在观察器中建立属性的初始值 - 通过指定
NSKeyValueObservingOptionPrior
,可以指示观察的对象再属性更改前发送通知。而在change dictionary
中将key为NSKeyValueChangeNotificationIsPriorKey
,value为@(YES)
,来表示这是一个预更改通知。在其他时候,这个键不存在。当观察者自己的KVO合规性要求其根据观察的属性触发-willChange...
方法。通常post-change notification来不及调用willChange...
context参数
context参数为上下文,其指针可以包含任意数据,这些数据将会在响应的更改通知中传递回观察者可以指定为NULL
,完全依赖keypath字符串来决定接收到的change notification的来源,但是这种方法当对象的父类也因为某些原因而 监听(observe)相同的keypath时可能会造成问题。
context参数内容 通常用于给接收方确认收到的属性改变通知的来源
我们通常以类中唯一命名的静态变量的地址作为context,这在父类或者子类中很难发生重复的。此时,可以为整个类选择一个上下文,然后依靠通知中的keyPath来确定更改内容,或者可以为每个观察的keyPath创建单独的上下文,而不需要进行字符串比较而进行通知的解析
//静态变量
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];
}
注意:
addObserver:forKeyPath:options:context:
并不对观察对象和context进行强引用,因此需要确保在必要时维护对观察对象和上下文的强引用
接收改变通知
当观察的属性改变时,observer会收到observeValueForKeyPath:ofObject:change:context:
消息,而所有的observer
必须实现这个方法
change dictionary中的NSKeyValueChangeKindKey
提供了有关发生的更改类型的信息。当观察对象的值修改时,则NSKeyValueChangeKindKey
对应条目的value值为NSKeyValueChangeSetting
。取决于注册时设定的option参数,更改字典中的NSKeyValueChangeOldKey、NSKeyValueChangeNewKey
条目中包含更改前后的属性值。如果属性是一个对象,则直接提供该值,而如果是标量或者C结构体,则改制将会包装在一个NSValue
对象中
如果观察的属性是一对多关系,那么NSKeyValueChangeKindKey
条目通过设置为NSKeyValueChangeInsertion
、NSKeyValueChangeRemoval
、NSKeyValueChangeReplacement
来插入、移除和替换关系中的对象
NSKeyValueChangeIndexesKey
的字典条目是一个NSIndexSet对象,用于指定已经更改关系中的索引。如果在注册observer时,将option参数设置为NSKeyValueObservingOptionNew或NSKeyValueObservingOptionOld
, 则change dictionary中的是包含更改前后相关对象值的数组
//接收到通知
- (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];
}
}
注意
无论如何 在observer无法识别context或者keypath不匹配时,应该始终调用父类的
observeValueForKeyPath:ofObject:change:context
当通知传播到基类,NSObject会触发
NSInternalInconsistencyException
错误,因为这是编程错误,子类没有相应其注册的通知
移除对象的观察者
通过向被观察对象发送removeObserver:forKeyPath:context:
消息移除,指定移除的观察者、keyPath、context、
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
注意点:
- 如果被移除的观察者并不是观察者,就会触发NSRangeEcxeption错误。如果无法确保存在,请将其写在
try...catch...
块中 - 在观察者dealloc后,观察者并不会主动删除自己。此时被观察对象继续发送通知,此时就会触发内存访问异常。
- 因为此协议无法查询一个对象是观察者或者被观察者,因此尽量避免代码发生混淆错误。可以在init或者viewDidLoad方法中注册,在dealloc方法中移除。这样不仅可以确保正确的注册和移除,并且保证在对象释放之前移除观察者
KVO规矩
为了的类的属性兼容KVO,类必须保证以下各项:
- 该类必须符合KVC,就如Ensuring KVC Compliance中所要求的
KVO支持与KVC相同的数据类型,包括Object-C对象以及标量和结构体支持列表中列出的标量和结构体 - 该类可以为属性发出KVO 的change notifications
- 该key已经被正确的注册
有两种技术可以确保发送change notification:
- NSObject提供了自动支持,默认情况下可以用于符合KVC的类的所有属性。
- 手动更改通知,可以对发送通知的时间提供额外的控制,并且需要其他编码。可以通过实现
automaticallyNotifiesObserversForKey:
方法来控制子类属性的自动通知
自动改变通知
NSObject提供自动键值更改通知的基本实现。自动键值更改通知将使用Key-value兼容访问器,和KVC方法进行的更改通知给观察者。由mutableArrayValueForKey:
返回的Collection代理对象也支持自动通知。
以下中的所有观察者都能收到更改的通知
// 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];
手动改变通知
某些情况下,希望控制通知进程,
手动和自动的通知并不是互斥的。除了已经发布的自动通知外,可以发布手动通知。
如果你想完全控制属性的通知,可以覆盖NSObject实现的automaticNotifyObserversForKey:
方法,在子类中返回NO。子类的自定义实现中,对于未识别的key应该调用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 {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
//也可以自己添加代码检查属性是否确认已经更改来发送不必要的通知
- (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"];
}
willChangeValueForKey:
方法会触发NSKeyValueObservingOptionPrior
option,而只有包含willChangeValueForKey
和didChangeValueForKey:
两者 才能触发改变通知optionNSKeyValueObservingOptionNew
注意
在自动更改通知中,是不会检查new值是否和old值相同的
如果是在一对多的关系中,不仅必须指定已经更改的key,必须指定更改的类型和所涉及对象的索引。更改类型时NSKeyCalueChange
,指定为NSKeyValueChangeInsertion,NSKeyValueChangeRemoval或NSKeyValueChangeReplacement
,而受影响的索引为NSIndexSet
对象
- (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"];
}
注册依赖键值
很多情况下,一个属性值依赖于一个或者多个其他对象中的属性。此时,如果一个属性值发生改变,那么派生的属性值也应该标记为更改。如何确保为这些从属属性发送kvo通知取决于其关系的基数
一对一关系
如果要触发一对一关系,需要重写keyPathsForValuesAffectingValueForKey:
方法,或者实现一个遵循注册依赖key键值模式的合适方法
例如:
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
此时当firstName或者lastName
更改时,必须要通知观察fullName
属性,因为其影响到了该属性值
方法1:重写keyPathsForValuesAffectingValueForKey:
,指定该属性取决于lastName
和fristName
属性
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
//通常需要调用父类方法的实现 避免干扰
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
方法2: 可以实现遵循keyPathsForValuesAffectingKey
的类方法,其Key为首字母大写的属性名称
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
注意:
当使用category将计算属性添加到类时,不能重写
keyPathsForValuesAffectingValueForKey:
方法。此时,请实现匹配了keyPathsForValuesAffecting Key
的类方法 来使用此机制(因为category类中实现此方法的话 就覆盖本类方法的实现)不能使用
keyPathsForValuesAffectingValueForKey
来建立对多关系的依赖
对多关系
keyPathsForValuesAffectingValueForKey:
不支持对多关系的keyPath。
可能有两种解决方案:
解决方案1
使用KVO注册父项为观察者,观察所有子项的相关属性,此时当子项添加和删除时 必须负责添加和删除观察者。在observeValueForKeyPath:ofObject:change:context
方法中更新依赖值来响应更改- (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;
}如果您使用的是Core Data,则可以将父项注册到应用程序的notification center,作为其对象上下文的观察者。父项应以类似于观察键值的方式响应子项发布的相关变更通知
KVO实现细节
自动KVO是使用isa-swizzling
技术实现的。
isa指针指向维护对象类的分发表。该表实际上包含了指向该类实现的方法指针和其他数据
在为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真时的class。因此,isa指针的值 并不一定反映实例的实际类
我们永远都不应该使用isa指针来确定类成员的身份,相反,我们可以使用class方法来确定对象实例的类