KVC
什么是KVC?
KVC(Key-value coding)是一種通過字符串去識別并間接存取(access)對象屬性的機制, 該機制區(qū)別于直接通過存取方法(accessor)和實例變量去訪問. 本質上, KVC定義了實現存取方法的模式與方法簽名.
KVC可用來訪問三種不同類型的對象值: attribute, 一對一關系, 一對多關系.
KVC方法
讀:
-valueForKey:
-valueForKeyPath:
-dicitionaryWithValuesForKeys:
對應key的值不存在時將發(fā)送消息valueForUndefinedKey:
給自己, 該方法默認實現拋出NSUndefinedKeyException
. 可自行重寫該方法.
寫:
-setValue:ForKey:
-setValue:ForKeyPath:
-setValuesForKeysWithDicitonary:
若指定的key不存在調用者將被發(fā)送消息setValue:forUndefinedKey:
, 同樣該方法默認拋出NSUndefinedKeyException
.
若把nil賦給一個非對象類型的屬性, 調用者被發(fā)送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類的實現文件里, 加上了一個裝有一堆質數的靜態(tài)數組, 以及定義了兩個方法.
瞧這兩方法是否看起來類似于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)
- 注冊觀察者:發(fā)送消息
addObserver:forKeyPath:options:context:
到被觀察對象. - 觀察著對象需實現
observeValueForKeyPath:ofObject:change:context:
方法.
而屬性為KVO兼容的, 亦需滿足三個條件:
- 它是KVC兼容的;
- 所在類會為該屬性發(fā)送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的自動與手動通知
以上例子為自動通知. 實際上蘋果系統(tǒng)framework中類的屬性都支持發(fā)送自動通知.
而手動通知可實現精細的控制通知發(fā)送. 手動通知通過重寫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中國