本文對 KVC、KVO 相關(guān)知識進(jìn)行全面的整理總結(jié),介紹了相關(guān)的基本概念、使用方法、注意事項、實現(xiàn)原理等。后續(xù)如有更深的理解會繼續(xù)整理總結(jié)。
簡介
KVC ( Key-value coding 鍵值編碼 ) 是一種由 NSKeyValueCoding 非正式協(xié)議啟用的機(jī)制,對象采用該機(jī)制提供對其屬性的間接訪問。當(dāng)對象符合鍵值編碼時,通過字符串名稱訪問對象屬性。
鍵值編碼的機(jī)制也是其他 Cocoa 框架的基礎(chǔ),例如 KVO。
KVO ( Key-value observing 鍵值觀察 ) 這一機(jī)制基于 NSKeyValueObserving 非正式協(xié)議,Cocoa 通過這個協(xié)議為所有遵守協(xié)議的對象提供了一種自動化的屬性觀察能力。對目標(biāo)對象的某屬性添加觀察,當(dāng)該屬性發(fā)生變化時,通過觸發(fā)觀察者對象實現(xiàn)的 KVO 接口方法,來通知觀察者。KVO 是 Cocoa 框架使用觀察者模式的一種途徑。
KVC
基本使用方法
KVC 提供了簡潔的方法,來訪問對象屬性。
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
上面兩個方法,分別是對應(yīng)于 getter 訪問器的 valueForKey: 和對應(yīng)于 setter 訪問器的 setValue:forKey: 。
- valueForKey: 首先查找以鍵 -getKey、 -key 或 -isKey 命名的 getter 方法。如果不存在 getter 方法(假設(shè)沒有通過@synthesize提供存取方法),它將在對象內(nèi)部查找名為 _key 或 key 的實例變量。如果最后沒找調(diào)用 valueForUndefinedKey: 方法。
- setValue:forKey: 首先查找以鍵 -setKey、 -_setKey 命名的 setter 方法,如果不存在 setter 方法,它將在類中查找名為 _key 或 key 的實例變量。如果最后沒找到則調(diào)用 setValue:forUndefinedKey: 方法。
例如某對象有屬性 name、age,我們就可使用上面兩個方法進(jìn)行訪問、設(shè)置屬性值。
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
[obj setValue:@"jone" forKey:@"name"];
[obj setValue:@(10) forKey:@"age"];
id stAge = [obj valueForKey:@"age"];
對于屬性是基本的數(shù)據(jù)類型時 (int, CGFloat) 是放入 NSNumber 或 NSValue 中來設(shè)置的。
相比直接訪問,KVC的效率會稍低一點,所以只有當(dāng)你非常需要它提供的可擴(kuò)展性時才使用它。
其他使用方法
1、屬性的屬性的訪問
KVC 還提供了訪問屬性的屬性的操作方法:
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
例如下面兩個類:
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end
@interface Teacher : NSObject
@property (nonatomic, strong) Student *student;
@end
//路徑訪問
[teacher setValue:@"haha" forKeyPath:@"student.name"];
id name = [teacher valueForKeyPath:@"student.name"];
2、多值訪問
同時訪問多個屬性的方法:
[obj setValuesForKeysWithDictionary:@{@"name":@"Tom", @"age":@(2)}];
NSDictionary *values = [obj dictionaryWithValuesForKeys:@[@"name",@"age"]];
3、集合屬性
KVC 同樣適用于集合對象,可以通過 valueForKey: 和 setValue:forKey:(或它們的鍵路徑方式)獲取或設(shè)置集合對象。
//@property (nonatomic, copy) NSArray<Student *> *students;
id array = [teacher valueForKeyPath:@"students.name"];
//返回數(shù)組,包含屬性 student.name
KVC 還提供了接口 mutableArrayValueForKey:、 mutableSetValueForKey: 來操作集合類型的屬性。
//@property (nonatomic, copy) NSArray *items;
obj.items = @[@"a", @"b", @"c"];
NSMutableArray *items = [obj mutableArrayValueForKey:@"items"];
[items addObject:@"d"];
//添加后,同時也改變了 obj.items
4、運(yùn)算符
運(yùn)算符是一個特殊的 Key Path,可以作為參數(shù)傳遞給 valueForKeyPath:方法,注意只能是這個方法,如果傳給了valueForKey:方法會崩潰。
運(yùn)算符是一個以@開頭的特殊字符串:

- 簡單集合運(yùn)算符有 @avg,@count,@max,@min,@sum
- 對象運(yùn)算符,比集合運(yùn)算符稍微復(fù)雜,能以數(shù)組的方式返回指定的內(nèi)容,有兩種 @distinctUnionOfObjects、@unionOfObjects ,前者會去除重復(fù)的以后返回,后者直接返回。
- Array和Set操作符,這種情況更復(fù)雜了,說的是集合中包含集合的情況,有三種 @distinctUnionOfArrays、@unionOfArrays、@distinctUnionOfSets,前兩個針對的集合是Arrays,后一個針對的集合是Sets。因為Sets中的元素本身就是唯一的,所以沒有對應(yīng)的 @unionOfSets 運(yùn)算符。
NSNumber *value = [teacher valueForKeyPath:@"students.@max.age"];
NSNumber *count = [teacher valueForKeyPath:@"students.@count"];
NSArray * array = [teacher valueForKeyPath:@"students.@distinctUnionOfObjects.age"];
NSMutableArray *someStudents = [NSMutableArray array];
[someStudents addObject:@[st0, st1, st2]];
[someStudents addObject:@[st3, st4]];
id value = [someStudents valueForKeyPath:@"@distinctUnionOfArrays.age"];
異常情況
1、找不到對應(yīng)的 key
當(dāng)調(diào)用 setValue:forKey: 或者 valueForKey: 找不到對應(yīng) key 命名的屬性時,就會 NSUnknownKeyException 異常崩潰,可以在對象里重寫下面兩個方法,防止崩潰。
- (id)valueForUndefinedKey:(NSString *)key {
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
}
2、將不是對象類型的屬性設(shè)置為 nil
將對象賦值為 nil 這是可以的相當(dāng)于把對象置空。但是當(dāng)使用 setValue:forKey: 將非對象類型的屬性值( int、CGFloat、結(jié)構(gòu)體等),設(shè)置為 nil 時會 NSInvalidArgumentException 異常崩潰。我們可以重寫方法 setNilValueForKey: 處理設(shè)置為 nil 的情況:
//@property (nonatomic, assign) int age;
//[obj setValue:nil forKey:@"age"];
- (void)setNilValueForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
[self setValue:@(0) forKey:@"age"];
} else {
[super setNilValueForKey:key];
}
}
KVO
KVO 是 Cocoa 框架使用觀察者模式的一種途徑。 KVC 是 KVO 技術(shù)實現(xiàn)的基礎(chǔ) ,參與 KVO 的對象需要符合 KVC 的要求和存取方法,也可以手動實現(xiàn)觀察者通知。
使用方法
1、添加觀察:
[obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:(__bridge void *)self];
options 回調(diào)選項,NSKeyValueObservingOptionOld 表示獲取舊值,NSKeyValueObservingOptionNew 表示獲取新值,NSKeyValueObservingOptionInitial 表示在添加觀察的時候就立馬響應(yīng)一個回調(diào),NSKeyValueObservingOptionPrior 表示在被觀察屬性變化前后都回調(diào)一次。
context 可以是 C 指針或者一個對象引用,既可以當(dāng)作一個唯一的標(biāo)識來分辨被觀察的變更,也可以向觀察者提供數(shù)據(jù)。
2、觀察回調(diào):
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"observe key = %@, obj = %@, change = %@", keyPath, object, change);
}
/*
observe key = name, obj = <Student: 0x6000016c6e80>, change = {
kind = 1;
new = Tony;
old = Tom;
}
*/
change 是一個字典,對應(yīng)的鍵有:NSKeyValueChangeKindKey、NSKeyValueChangeNewKey、NSKeyValueChangeOldKey、NSKeyValueChangeIndexesKey、NSKeyValueChangeNotificationIsPriorKey。
NSKeyValueChangeKindKey 指明了變更類型,設(shè)置、插入、移除、替換:
enum {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
NSKeyValueChangeNotificationIsPriorKey 指明是變更前或變更后,觸發(fā)的回調(diào)。
3、移除觀察:
[obj removeObserver:self forKeyPath:@"name"];
移除觀察,和移除通知比較類似,需要在不用繼續(xù)觀察的時候移除它,比如在控制器的 dealloc 方法里面釋放,注意重復(fù)移除會 crash。
4、調(diào)試 KVO
可以打斷點,在 lldb 中查看被觀察對象的所有觀察信息。
lldb po [obj observationInfo]
這會打印出有關(guān)誰觀察誰之類的很多信息。
KVO 兼容
有兩種方法可以保證變更通知被發(fā)出。自動發(fā)送通知是 NSObject 提供的,并且一個類中的所有屬性都默認(rèn)支持,只要是符合 KVC 的。
手動變更通知需要些額外的代碼,但也對通知發(fā)送提供了額外的控制??梢酝ㄟ^重寫子類 automaticallyNotifiesObserversForKey: 方法的方式控制子類一些屬性的自動通知。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
- (void)setName:(NSString *)name {
if (![_name isEqualToString:name]) {
[self willChangeValueForKey:@"name"];
_name = [name copy];
[self didChangeValueForKey:@"name"];
}
}
//如果一個操作導(dǎo)致多個鍵變化,需要嵌套變更通知
- (void)setLastName:(NSString *)lastName {
[self willChangeValueForKey:@"lastName"];
[self willChangeValueForKey:@"fullName"];
_lastName = [lastName copy];
_fullName = [NSString stringWithFormat:@"Title %@", lastName];
[self didChangeValueForKey:@"fullName"];
[self didChangeValueForKey:@"lastName"];
}
當(dāng)觀察某個對象的集合屬性時,當(dāng)直接使用 obj.mutableArray 添加、刪除、替換元素時,不會觸發(fā)觀察回調(diào),需要手動添加代碼 willChange:valuesAtIndexes:forKey: 和 didChange:valuesAtIndexes:forKey: 來通知集合屬性發(fā)生了變化?;蛘呤褂?KVC 來操作集合屬性。如下例子:
//某類集合屬性
//@property (nonatomic, strong) NSMutableArray *myArray;
//添加觀察
[obj addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
//變更集合時,手動通知觀察者
[self.obj willChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:0] forKey:@"myArray"];
[self.obj.myArray removeObjectAtIndex:0];
[self.obj didChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:0] forKey:@"myArray"];
//或者使用 KVC 操作集合,會自動通知觀察者
NSMutableArray *array = [self.obj mutableArrayValueForKey:@"myArray"];
[array removeObjectAtIndex:0];
注冊從屬鍵
某些情況下,一個屬性的值取決于另一個對象中的一個或多個其他屬性的值。如果一個屬性的值發(fā)生更改,則還應(yīng)標(biāo)記派生屬性的值以進(jìn)行更改。
例如,fullName 取決于 firstName 和 lastName。當(dāng) firstName 或 lastName 發(fā)生改變時,必須通知觀察 fullName 屬性的程序,因為它們影響這個屬性的值。
重寫 keyPathsForValuesAffectingValueForKey 來指定 fullName 屬性依賴于lastName和firstName。
- (void)setLastName:(NSString *)lastName {
_lastName = [lastName copy];
_fullName = [NSString stringWithFormat:@"%@ %@", _firstName, lastName];
}
//重寫指定 fullName 屬性依賴于lastName
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
//或者重寫
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
基本原理
KVO 是基于 runtime 運(yùn)行時來實現(xiàn)的,當(dāng)你觀察了某個對象的屬性,內(nèi)部會生成一個該對象所屬類的子類,中間類,然后重寫被觀察屬性的 setter 方法,當(dāng)然在重寫的方法中會調(diào)用父類的 setter 方法從而不會影響框架使用者的邏輯,之后會將該對象的 isa 指針指向新創(chuàng)建的這個類,最后會重寫 -(Class)class; 方法,讓使用者通過 [obj class] 查看當(dāng)前對象所屬類的時候會返回其父類,使其看似沒有改變什么,讓你覺得不需要添加額外的代碼,就能使用 KVO。
Apple 并不希望過多暴露 KVO 的實現(xiàn)細(xì)節(jié)。想要深究實現(xiàn)細(xì)節(jié),可查看文章
下面例子,通過 object_getClass() 方法查看觀察前后的變化。
//object_getClass 獲取 isa 指針指向的對象
//object_setClass 更改對象的 isa 指針指向。將對象設(shè)置為別的類類型,返回原來的Class
NSLog(@"1 --- %p %@ %p", obj, object_getClass(obj), [obj methodForSelector:@selector(setName:)]);
[obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:(__bridge void *)self];
NSLog(@"2 --- %p %@ %p", obj, object_getClass(obj), [obj methodForSelector:@selector(setName:)]);
[self.obj removeObserver:self forKeyPath:@"name"];
NSLog(@"3 --- %p %@ %p", self.obj, object_getClass(self.obj), [self.obj methodForSelector:@selector(setName:)]);
//1 --- 0x60000225cea0 AStudent 0x1067f72a0
//2 --- 0x60000225cea0 NSKVONotifying_AStudent 0x7fff258e454b
//3 --- 0x60000225cea0 AStudent 0x1067f72a0
通過上面的打印結(jié)果可知,添加觀察后,原本的類變成了 NSKVONotifying_AStudent,移除觀察后又變回去了, setName: 方法也發(fā)生了變化。