KVC
什么是KVC?
KVC(Key-value coding)是一种通过字符串去识别并间接存取(access)对象属性的机制, 该机制区别于直接通过存取方法(accessor)和实例变量去访问. 本质上, KVC定义了实现存取方法的模式与方法签名.
KVC可用来访问三种不同类型的对象值: attribute, 一对一关系, 一对多关系.
KVC方法
读:
-valueForKey:
-valueForKeyPath:
-dicitionaryWithValuesForKeys:
对应key的值不存在时将发送消息valueForUndefinedKey:
给自己, 该方法默认实现抛出NSUndefinedKeyException
. 可自行重写该方法.
写:
-setValue:ForKey:
-setValue:ForKeyPath:
-setValuesForKeysWithDicitonary:
若指定的key不存在调用者将被发送消息setValue:forUndefinedKey:
, 同样该方法默认抛出NSUndefinedKeyException
.
若把nil赋给一个非对象类型的属性, 调用者被发送setNilValueForKey:
消息, 该方法默认抛出NSInvalidArgumentException.
自己可重写该方法来实现正确的赋值. 例如:
// MyModel.h
@interface MyModel : NSObject
@property (nonatomic, assign) BOOL hidden;
@property (nonatomic, assign, readonly) NSInteger num;
@end
// MyModel.m
@implementation MyModel
- (void)setNilValueForKey:(NSString *)key {
if ([key isEqualToString:@"num"]) {
[self setValue:@0 forKey:@"num"];
} else if ([key isEqualToString:@"hidden"]) {
[self setValue:@NO forKey:@"hidden"];
} else {
[super setNilValueForKey:key];
}
}
@end
KVC与点语法访问方法
两种方法可同时并存使用.
如下例所示, 定义了一个类:
@interface MyClass
@property NSString *stringProperty;
@property NSInteger integerProperty;
@property MyClass *linkedInstance;
@end
用KVC访问属性如下:
MyClass *myInstance = [[MyClass alloc] init];
NSString *string = [myInstance valueForKey:@"stringProperty"];
[myInstance setValue:@2 forKey:@"integerProperty"];
以下两种方式结果是一样的:
MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
myInstance.linkedInstance.integerProperty = 2;
MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
[myInstance setValue:@2 forKeyPath:@"linkedInstance.integerProperty"];
KVC兼容(Key-value coding compliant)
KVC兼容是对于类中某个特定属性(property)来说的, 所谓KVC兼容实际指的是某个属性可通过-valueForKey:
, -setValue:forKey:
等KVC方法去访问属性.
在Objective-C 2.0里的@property实质上也就是一对setter, getter访问器方法加一个实例变量.
先看个例子:
//MyModel.m
@implementation
...
{
NSObject *_myObj;
}
- (NSObject *)myObj {
if (!_myObj) {
_myObj = [[NSObject alloc] init];
}
return _myObj;
}
- (void)setMyObj:(NSObject *)obj {
_myObj = obj;
}
@end
调用时,
NSLog(@"MyModel myObj: %@", model.myObj);
NSObject *obj = [[NSObject alloc] init];
NSLog(@"auto obj: %@", obj);
[model setValue:obj forKey:@"myObj"];
NSLog(@"MyModel myObj: %@", model.myObj);
打印出来:
2016-06-30 15:19:26.991 TestKVC[19217:20399799] MyModel myObj: <NSObject: 0x7fea43424fe0>
2016-06-30 15:19:26.991 TestKVC[19217:20399799] auto obj: <NSObject: 0x7fea45813d90>
2016-06-30 15:19:26.991 TestKVC[19217:20399799] MyModel myObj: <NSObject: 0x7fea45813d90>
以上, 对于myObj这个属性来说, 它就是KVC兼容的.
其实, 没有实例变量也是可以的.
// MyModel.m
...
- (NSObject *)noSuchObj {
NSLog(@"getter method");
return nil;
}
- (void)setNoSuchObj:(NSObject *)obj {
NSLog(@"setter method");
}
调用时,
//invoke getter accessor
[model valueForKey:@"noSuchObj"];
//invoke setter accessor
[model setValue:@"nothing" forKey:@"noSuchObj"];
//same as the above one for dot syntax
model.noSuchObj = @"nothing";
打印出来是,
2016-06-30 15:19:26.992 TestKVC[19217:20399799] getter method
2016-06-30 15:19:31.439 TestKVC[19217:20399799] setter method
2016-06-30 15:19:38.882 TestKVC[19217:20399799] setter method
实际上-valueForKey:
, -setValue:forKey:
之类的KVC方法在运行时会按照一定的顺序去调用遵循特定方法签名的访问器方法或直接访问实例变量.
例如-setValue:forKey:
这个方法的实现大概是这样的:
- 查看调用对象所属类是否实现了
-set<Key>:
这样的访问器方法, 有则调用; - 若没有, 调用者类方法
-accessInstanceVariablesDirectly
放回YES, 然后依次搜索看是否存在命名方式为_<key>
,_is<Key>
,<key>
,is<Key>
这样的实例变量, 有则直接赋值于它. - 若遵循这种命名形式的访问器方法和实例变量都无, 则调用
setValue:forUndefinedKey:
方法.
一对多关系属性的KVC, 集合代理
先看个例子:
//MyModel.m
...
static int32_t const primes[] = {
2, 101, 233, 383, 3, 103, 239, 389, 5, 107, 241, 397, 7, 109,
251, 401, 11, 113, 257, 409, 13, 127, 263, 419, 17, 131, 269,
421, 19, 137, 271, 431, 23, 139, 277, 433, 29, 149, 281, 439,
31, 151, 283, 443, 37, 157, 293, 449, 41, 163, 307, 457, 43,
167, 311, 461, 47, 173, 313, 463, 53, 179, 317, 467, 59, 181,
331, 479, 61, 191, 337, 487, 67, 193, 347, 491, 71, 197, 349,
499, 73, 199, 353, 503, 79, 211, 359, 509, 83, 223, 367, 521,
89, 227, 373, 523, 97, 229, 379, 541, 547, 701, 877, 1049,
557, 709, 881, 1051, 563, 719, 883, 1061, 569, 727, 887,
1063, 571, 733, 907, 1069, 577, 739, 911, 1087, 587, 743,
919, 1091, 593, 751, 929, 1093, 599, 757, 937, 1097, 601,
761, 941, 1103, 607, 769, 947, 1109, 613, 773, 953, 1117,
617, 787, 967, 1123, 619, 797, 971, 1129, 631, 809, 977,
1151, 641, 811, 983, 1153, 643, 821, 991, 1163, 647, 823,
997, 1171, 653, 827, 1009, 1181, 659, 829, 1013, 1187, 661,
839, 1019, 1193, 673, 853, 1021, 1201, 677, 857, 1031,
1213, 683, 859, 1033, 1217, 691, 863, 1039, 1223, 1229,
};
- (NSUInteger)countOfPrimes;
{
return (sizeof(primes) / sizeof(*primes));
}
- (id)objectInPrimesAtIndex:(NSUInteger)idx;
{
NSParameterAssert(idx < sizeof(primes) / sizeof(*primes));
return @(primes[idx]);
}
在上述MyModel类的实现文件里, 加上了一个装有一堆质数的静态数组, 以及定义了两个方法.
瞧这两方法是否看起来类似于NSArray的原始(primitive)方法 -count
和-objectAtInIndex:
呢?
使用时:
//invoke collection accessors
id proxy = [model valueForKey:@"primes"];
NSLog(@"MyModel last prime: %@", [proxy lastObject]);
这里我们是把primes
当做一个NSArray来使用的. 注意上面例子中并没有声明有primes
这么一个属性的, 也无这么一个实例变量对象. 我们可以通过valueForKey:
去获取, 那么它一定是实现KVC了.
实际上, 这里valueForKey:
方法内部大概是这样的:
- 依次查找
get<Key>
,<key>
,is<Key>
形式的访问器方法. - 找不到则查找匹配模式
countOf<Key>
和objectIn<Key>AtIndex:
(或objectsAtIndexes:
)的方法. 若找到, 则返回一个集合代理对象(collection proxy object), 该对象可使用所有NSArray的方法. - 找不到则查找
countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>:
, 同理如上, 不过对应的是NSSet, 无序集合. - 再找不到则查找匹配命名
_<key>
,_is<Key>
,<key>
, oris<Key>
的实例变量. - 否则调用
valueForUndefinedKey:
.
上例中的proxy就是一个集合代理对象, 调用[proxy class]
得知它是一个NSKeyValueArray类型.
集合代理对象里, 包括了有序与无序, 可变与不可变的不同情况, 其分别也对应于实现不同的方法以KVC兼容. 但它们机理都是类似的.
集合运算符
- 简单运算符:@avg, @count, @max, @min. @sum.
- 对象运算符:@distinctUnionOfObjects, @unionOfObjects.
- 数组,集合运算符:@distinctUnionOfArrays, @unionOfArrays, @distinctUnionOfSets.
上例中,
id proxy = [model valueForKey:@"primes"];
NSLog(@"MyModel primes count: %lu", [proxy count]);
NSLog(@"MyModel primes count: %@", [model valueForKeyPath:@"primes.@count"]);
打印的两个count结果都是一样的, 都是201
.
KVO
接收一个属性的KVO通知, 需三个条件:
- 被观察的属性是KVO兼容的.(KVO Compliant)
- 注册观察者:发送消息
addObserver:forKeyPath:options:context:
到被观察对象. - 观察着对象需实现
observeValueForKeyPath:ofObject:change:context:
方法.
而属性为KVO兼容的, 亦需满足三个条件:
- 它是KVC兼容的;
- 所在类会为该属性发送KVO通知;
- 依赖键需注册.
看个例子. 以下两方法都在都一观察者类中, self即observer.
// 观察者类中注册键值观察
- (void)registerAsObserver {
self.model = [[MyModel alloc] init];
[self.model addObserver:self
forKeyPath:@"num"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.model setValue:@"999" forKey:@"num"];
});
}
// 实现处理KVO通知方法
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
if (object == self.model) {
if ([keyPath isEqualToString:@"num"]) {
NSLog(@"old num:%@", change[NSKeyValueChangeOldKey]);
NSLog(@"new num:%@", change[NSKeyValueChangeNewKey]);
}
}
}
这里打印被观察属性值变化的新旧值.
KVO的自动与手动通知
以上例子为自动通知. 实际上苹果系统framework中类的属性都支持发送自动通知.
而手动通知可实现精细的控制通知发送. 手动通知通过重写NSObject的automaticallyNotifiesObserversForKey:
方法判断是否自动.
用法如下例所示:
//MyModel.m
...
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"num"]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
NO
表示键为num
的属性为手动KVO.
重写其setter方法:
//MyModel.m
...
- (void)setNum:(NSInteger)num {
if (num != _num) {
[self willChangeValueForKey:@"num"];
_num = num;
[self didChangeValueForKey:@"num"];
}
}
参考资料:
Key-Value Observing Programming Guide
Key-Value Coding Programming Guide
KVC 和 KVO objc中国