一、前言
Objective-C 中的键(key)-值(value)观察(KVO)并不是什么新鲜事物,它来源于设计模式中的观察者模式,其基本思想就是:
一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。
在 Objective-C 中有两种使用键值观察的方式:手动或自动,此外还支持注册依赖键(即一个键依赖于其他键,其他键的变化也会作用到该键)。下面将一一讲述这些,并会深入 Objective-C 内部一窥键值观察是如何实现的。
二、KVO机制
1、注册与解除注册
如果已经有了包含可供KVO属性的类(即被观察类),那么就可以通过在该类对象(被观察类对象)上调用名为 NSKeyValueObserverRegistration 的 category 方法将观察者对象与被观察者对象进行注册与解除注册:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
这个三个方法的定义在这两个方法的定义在 Foundation/NSKeyValueObserving.h 中,NSObject,NSArray,NSSet均实现了以上方法,因此我们不仅可以观察普通对象,还可以观察数组或集合类对象。
提示:不要忘记解除注册,否则会导致资源泄露。
2、处理变更通知
当被观察者类对象中某属性发生变更,观察者需要处理接收到的变更通知。在观察者类中,需要实现名为 NSKeyValueObserving 的 category 方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
其中:change 这个字典保存了哪些变更信息,取决于注册时的 NSKeyValueObservingOptions
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,
NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08
};
3、手动或者自动实现KVO通知
KVO机制提供两种变更消息通知模式:手动实现,自动实现。
在 NSKeyValueObservingCustomization 的 category 中有方法:
/* Return YES if the key-value observing machinery should automatically invoke -willChangeValueForKey:/-didChangeValueForKey:, -willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey:, or -willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects: whenever instances of the class receive key-value coding messages for the key, or mutating key-value coding-compliant methods for the key are invoked. Return NO otherwise. Starting in Mac OS 10.5, the default implementation of this method searches the receiving class for a method whose name matches the pattern +automaticallyNotifiesObserversOf<Key>, and returns the result of invoking that method if it is found. So, any such method must return BOOL too. If no such method is found YES is returned.
*/
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
默认情况下,KVO是采用自动实现的。其会自动调用 NSKeyValueObserverNotification 的 category 方法:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
或:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
或:
- (void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;
- (void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;
如果要采用手动实现,则要实现 automaticallyNotifiesObserversForKey 方法,return NO。
并且在 被观察者对象属性 setter 方法中手动 调用 NSKeyValueObserverNotification 的 category 方法。
例如:被观察者对象属性:lComponent
+ (BOOL)automaticallyNotifiesObserversForLComponent;
{
return NO;
}
- (void)setLComponent:(double)lComponent;
{
if (_lComponent == lComponent) {
return;
}
[self willChangeValueForKey:@"lComponent"];
_lComponent = lComponent;
[self didChangeValueForKey:@"lComponent"];
}
对于第一、二、三点,给出一个例子:
观察者类:
#import "Observer.h"
#import <objc/runtime.h>
#import "Target.h"
@implementation Observer
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqualToString:@"age"])
{
Class classInfo = (__bridge Class)context;
NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
encoding:NSUTF8StringEncoding];
NSLog(@" >> class: %@, Age changed", className);
NSLog(@" old age is %@", [change objectForKey:@"old"]);
NSLog(@" new age is %@", [change objectForKey:@"new"]);
}
else
{
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
@end
被观察者类:
①自动KVO变更通知
#import <Foundation/Foundation.h>
@interface Target : NSObject
@property(nonatomic,assign)int age;
@end
#import "Target.h"
@implementation Target
@synthesize age;
-(instancetype)init
{
self = [super init];
if (nil != self) {
age = 10;
}
return self;
}
@end
②手动KVO变更通知
#import <Foundation/Foundation.h>
@interface Target : NSObject
{
int age;
}
-(int)age;
-(void)setAge:(int)theAge;
@end
#import "Target.h"
@implementation Target
-(instancetype)init
{
self = [super init];
if (nil != self) {
age = 10;
}
return self;
}
-(int)age
{
return age;
}
-(void)setAge:(int)theAge
{
if (age == theAge) {
return;
}
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
主函数:
#import <Foundation/Foundation.h>
#import "Observer.h"
#import "Target.h"
int main(int argc, const char * argv[])
{
@autoreleasepool {
//观察者
Observer * observer = [[Observer alloc] init];
//被观察者
Target * target = [[Target alloc] init];
[target addObserver:observer
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:(__bridge void *)([Target class])];
[target setAge:30];
//[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
[target removeObserver:observer forKeyPath:@"age"];
}
return 0;
}
运行输出:
2014-01-28 17:23:47.408 Foun[1346:303] >> class: Target, Age changed
2014-01-28 17:23:47.422 Foun[1346:303] old age is 10
2014-01-28 17:23:47.423 Foun[1346:303] new age is 30
4、KVO 依赖键
有时候一个属性的值依赖于其他属性值,那么如果其他属性值发生变更,那么必然也就导致该属性值的变更,也即 Dependent Poroperties。在KVO中,引入了依赖键
在KVO中,提供了如下两种实现 依赖键 的方法:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
+ (NSSet *)keyPathsForValuesAffecting<Key>
例如, information 属性依赖于 target 的 age 和 grade 属性,target 的 age/grade 属性任一发生变化,information 的观察者都会得到通知。
+ (NSSet *)keyPathsForValuesAffectingInformation
{
NSSet * keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
return keyPaths;
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
NSArray * moreKeyPaths = nil;
if ([key isEqualToString:@"information"])
{
moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];
}
if (moreKeyPaths)
{
keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
}
return keyPaths;
}
要实现 keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法是告诉系统 information 属性依赖于哪些其他属性,这两个方法都返回一个key-path 的集合。
如果选择实现 keyPathsForValuesAffectingValueForKey,要先获取 super 返回的结果 set,然后判断 key 是不是目标 key,如果是就将依赖属性的 key-path 结合追加到 super 返回的结果 set 中,否则直接返回 super的结果。
注:显然前者实现过程简单。
假设某一个类对象通过
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
注册其本身即是观察者,又是被观察者。
那么就有必要传递一个 唯一的 context以作为标识
static int const PrivateKVOContext;
[otherObject addObserver:self forKeyPath:@"someKey" options:someOptions context:&PrivateKVOContext];
然后:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (context == &PrivateKVOContext) {
// Observe values here
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
6、关于NSKeyValueObservingOptions
在注册观察者的时候,即调用以下方法的时候,可以指定options属性。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
对于
NSKeyValueObservingOptions:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,
NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08
};
①指定 NSKeyValueObservingOptionInitial
调用 addObserver:forKeyPath:... 方法的时候就会触发 KVO 通知响应
②指定 NSKeyValueObservingOptionPrior
观察者会接收到两次通知:(1)在变更之前 (2)在变更之后
可以通过如下代码获取变更前后的值(change是一个字典)
if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
// Before the change
} else {
// After the change
}
③加入只需要获取变更前的值,或变更后的值,那么可以指定NSKeyValueObservingOptionNew 或 NSKeyValueObservingOptionOld 。
id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];
在KVO中,保存变更后的值和变更前的值是在以下两个方法调用的时候进行的:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
7、KVO和线程
KVO机制的运行是采取同步,单线程的方式。也即无论是采用手动或者自动的KVO变更通知,以下两个方法的调用都是处于同一个线程或者运行队列中。
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;