KVO(Key-Value Observing)
——键值观察,它是一种机制,它允许将其他对象的指定属性的更改,通知给另一个对象。KVO苹果文档
关于KVO
如何创建使用,大致分为三个步骤:
使用步骤
注册观察者
- 使用方法将观察者注册到观察对象addObserver:forKeyPath:options: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];
}
options
是一个枚举,包含了以下四个值:
NSKeyValueObservingOptionNew:
观察更改后的值;
NSKeyValueObservingOptionOld:
观察更改前的值;
NSKeyValueObservingOptionInitial:
观察最初的值(在注册观察服务时会调用一次触发方法);
NSKeyValueObservingOptionPrior:
分别在值修改前后触发方法(即一次修改有两次触发)
context
:上下文,包含任意数据,这些数据将在相应的更改通知中传递回观察者。可以指定NULL
并完全依赖KeyPath
字符串来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因而观察到相同的键路径,从而导致问题。
一种更安全,更可扩展的方法是使用上下文确保您收到的通知是发给观察者的,而不是超类的。在类中定义唯一命名的静态变量的地址,就满足了良好的上下文条件。在父类或子类中以类似方式选择的上下文不太可能重叠。可以为整个类选择一个上下文,然后依靠通知消息中的KeyPath
字符串来确定更改的内容。另外,可以为每个观察到的键路径创建一个不同的上下文,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析。上面示例中显示了以这种方式选择的balance
和interestRate
属性的示例上下文。
接受变更通知
- 在观察者内部实现observeValueForKeyPath:ofObject:change: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];
}
}
当对象的观察属性的值更改时,观察者会收到一条observeValueForKeyPath:ofObject:change:context:
消息。所有观察者都必须实现此方法。
移除观察者
- 当观察者不再应接收消息时,使用该方法removeObserver:forKeyPath:注销观察者。至少在观察者从内存释放之前调用注销方法,否则会导致奔溃。
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
注销观察者
典型的使用场景是在观察者初始化期间(例如,在init
或viewDidLoad
)注册为观察者,在释放过程中(通常在中dealloc)解除注册,以确保成对和有序地添加和删除消息,并确保观察者在从内存中释放之前被取消注册。
如果注册了观察者未注销,当再次进入观察者界面时,会再次注册KVO
观察者,导致KVO观察的重复注册,而第一次的通知对象还在内存中,没有进行释放。如果此时接收到了属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,将会导致类似野指针的崩溃,可理解为一直保持着一个野通知,且一直在监听。
问:多次添加注册未注销会不会造成循环引用?不会,因为
observer
在底层的字符串是weak修饰,所以不会导致循环引用。
自动 & 手动变更通知
自动变更通知
NSObject
提供自动的键值更改通知的基本实现。自动键值更改通知将使用键值兼容访问器(setName
)以及键值编码方法(setValue:forKey:
)进行的更改通知给观察者。由mutableArrayValueForKey:
返回的收集代理对象也支持自动通知。
以下显示的示例使该属性的所有观察者都name
收到有关更改的通知。
// 使用setter方法直接设置
[account setName:@"Savings"];
// 使用kvc设置name
[account setValue:@"Savings" forKey:@"name"];
// 使用keypath 设置document的name
[document setValue:@"Savings" forKeyPath:@"account.name"];
// 使用 mutableArrayValueForKey: to retrieve a relationship proxy object.
NSArray * arrayTrans = @{@"1001",@"1002"};
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject: arrayTrans];
手动变更通知
这是切换手动or自动的方法,默认YES
即为自动变更通知,这里可以判断theKey
来控制是否手动变更通知。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要实现手动观察者通知,在willChangeValueForKey:
更改值之前和didChangeValueForKey:
更改值之后调用。如下实现了该balance
属性的手动通知,首先检查值是否已更改来最大程度地减少发送不必要的通知。如下balance则可以实现仅在通知已更改时才提供通知。
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
网友评论