KVC — Key Value Coding 键值编码 KVC 是一种不用通过调用 setter、getter 方法而是直接通过属性字符串名称key来存取属性的机制。
KVC 和 属性访问器的对比如下:
总而言之 KVC的特点是在运行时访问,可以访问对象私有属性和修改只读属性,在字典转模型,集合类操作方面十分便捷。
首先先列下关键的一些方法,后续都会对这些方法进行介绍:
- (nullable id )valueForKeyPath:(NSString *)keyPath; - (void )setValue:(nullable id )value forKeyPath:(NSString *)keyPath; - (nullable id )valueForKey:(NSString *)key; - (void )setValue:(nullable id )value forKey:(NSString *)key; + (BOOL )accessInstanceVariablesDirectly; - (BOOL )validateValue:(inout id __nullable * __nonnull )ioValue forKey:(NSString *)inKey error:(out NSError **)outError; - (nullable id )valueForUndefinedKey:(NSString *)key; - (void )setValue:(nullable id )value forUndefinedKey:(NSString *)key; - (void )setNilValueForKey:(NSString *)key; - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; - (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key; - (NSMutableSet *)mutableSetValueForKey:(NSString *)key; - (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath; - (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath; - (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath; - (NSDictionary <NSString *, id > *)dictionaryWithValuesForKeys:(NSArray <NSString *> *)keys; - (void )setValuesForKeysWithDictionary:(NSDictionary <NSString *, id > *)keyedValues;
下面是KVC 关键用法的总结:
1. 设置和访问策略及访问私有属性和可读属性 #import <Foundation/Foundation.h> @interface Teacher : NSObject @property (nonatomic , strong , readonly ) NSString *readonlyValue;@end #import "Teacher.h" @interface Teacher ()@property (nonatomic , assign ,readwrite ) NSInteger age;@property (nonatomic , strong ,readwrite ) NSString *name;@property (nonatomic , assign ,readwrite ) BOOL male;@property (nonatomic , assign ,readwrite ) BOOL isTest;@end @implementation Teacher - (NSString *)description { return [NSString stringWithFormat:@"name = %@ \ age = %ld \ readOnlyValue = %@ \ is Male = %d" , self .name, (long )self .age, self .readonlyValue, self .male]; } int main(int argc, const char * argv[]) { @autoreleasepool { Teacher *teacher = [Teacher new]; [teacher setValue:@"linxiaohai" forKey:@"name" ]; NSLog (@"%@" ,teacher); [teacher setValue:@"linxiaohai1" forKey:@"_name" ]; NSLog (@"%@" ,teacher); [teacher setValue:@(29 ) forKey:@"age" ]; NSLog (@"%@" ,teacher); [teacher setValue:@(YES ) forKey:@"male" ]; NSLog (@"%@" ,teacher); [teacher setValue:@(NO ) forKey:@"_male" ]; NSLog (@"%@" ,teacher); [teacher setValue:@"This is a readOnly Value" forKey:@"readonlyValue" ]; NSLog (@"%@" ,teacher); [teacher setValue:@"This is a readOnly Value 2" forKey:@"_readonlyValue" ]; NSLog (@"%@" ,teacher); NSLog (@"Test: %d" , [[teacher valueForKey:@"test" ] boolValue]); [teacher setValue:@(YES ) forKey:@"test" ]; NSLog (@"%@" ,teacher); NSLog (@"Test: %d" , [[teacher valueForKey:@"test" ] boolValue]); } return 0 ; }
2019 -06 -21 11 :27 :36 .860598 +0800 KVC-Demo[42518 :5605320 ] name = linxiaohai age= 0 readOnlyValue = (null) is Male = 0 2019 -06 -21 11 :27 :36 .860744 +0800 KVC-Demo[42518 :5605320 ] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 0 2019 -06 -21 11 :27 :36 .860916 +0800 KVC-Demo[42518 :5605320 ] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 0 2019 -06 -21 11 :27 :36 .861087 +0800 KVC-Demo[42518 :5605320 ] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 1 2019 -06 -21 11 :27 :36 .861213 +0800 KVC-Demo[42518 :5605320 ] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value is Male = 1 2019 -06 -21 11 :27 :36 .861348 +0800 KVC-Demo[42518 :5605320 ] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value 2 is Male = 1 2019 -06 -21 11 :27 :36 .861422 +0800 KVC-Demo[42518 :5605320 ] male: 1 2019 -06 -21 11 :27 :36 .877241 +0800 KVC-Demo[42518 :5605320 ] ===================================================2019 -06 -21 11 :27 :36 .877501 +0800 KVC-Demo[42518 :5605320 ] Test: 0 2019 -06 -21 11 :27 :36 .877765 +0800 KVC-Demo[42518 :5605320 ] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value 2 is Male = 1 2019 -06 -21 11 :27 :36 .877824 +0800 KVC-Demo[42518 :5605320 ] Test: 1 2019 -06 -21 11 :27 :36 .877867 +0800 KVC-Demo[42518 :5605320 ] ===================================================
-(void)setValue:(id)value forKey:(NSString *)key 规则:
首先搜索 setter 方法,有就直接赋值。 如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly 返回 NO,则执行set Value:for UNdefinedKey: 返回 YES,则按_<key> ,_<isKey> ,<key> ,<isKey> 的顺序搜索[实例变量]注意这里是搜索实例变量,不是属性。 还没有找到的话,就调用set Value:for UndefinedKey:
总结来说就是KVC设值的顺序如下: setKey -> _key -> _isKey -> key -> isKey
-(id)valueForKey:(NSString *)key 规则
* 首先查找 getter 方法,找到直接调用。如果是 bool、int 、float 等基本数据类型,会做 NSNumber 的转换。 * 如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly * 返回 NO,则执行valueForUNdefinedKey: * 返回 YES,则按_<key> ,_is<Key> ,<key> ,is <Key> 的顺序搜索实例变量。 * 如果没有则查找countOf<Key> ,objectIn<Key> AtIndex或<Key> AtIndexes格式的方法,如果countOf<Key> 方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合 * 还没找到则查找countOf<Key> ,enumeratorOf<Key> ,memberOf<Key> 格式的方法,如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合。 * 还没有找到的话,调用valueForUndefinedKey:
总结来说就是KVC取值的顺序如下: getKey -> _key -> _isKey -> key -> isKey -> [countOf <Key> objectIn<Key>AtIndex <Key>AtIndexes] -> [countOf<Key>,enumeratorOf<Key>,memberOf<Key>]
keyPath 用法:
Teacher *teacher = [Teacher new];Student *student = [Student new];[student setValue:@"Jimmy" forKey:@"name" ]; [teacher setValue:student forKey:@"student" ]; NSString *studentName = [teacher valueForKeyPath:@"student.name" ];NSLog (@"Student name: %@" ,studentName);[teacher setValue:@"linxiaohai" forKeyPath:@"student.name" ];
2. 异常处理 如果都没找到对应的key就会调用setValue:forUndefinedKey: 和valueForUndefinedKey: 下面是一个例子:
增加 - (void)setValue:(id )value forUndefinedKey:(NSString *)key { if ([key isEqualToString:@"undefineKey" ]) { NSLog(@"=========> %@" ,value ); } } 在调用[teacher setValue:@"valueForUndefineKey" forKey:@"undefineKey" ] 的时候就会打出如下log =========> valueForUndefineKey 增加: - (id )valueForUndefinedKey:(NSString *)key { return [NSString stringWithFormat:@"This is A default Value for %@" ,key ]; } 在调用 NSLog(@"Test: %@" , [teacher valueForKey:@"undefineKey" ]); 就会打出如下Log: Test: This is A default Value for undefineKey
其他API
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; - (void )setNilValueForKey:(NSString *)key;
3. 字典转模型 使用字典来初始化模型
NSDictionary *catogiresDic = @{ @"id" :@1 , @"image" :@"@www.iPhone.com" , @"name" :@"iPhone" , @"price" :@"100.03" }; CategoryList *catogires = [[CategoryList alloc] init]; [catogires setValuesForKeysWithDictionary:catogiresDic];
通过key的集合来从一个model中取出值形成一个字典
NSDictionary *value = [catogires dictionaryWithValuesForKeys:@[@"name",@" image "]];
4. 集合操作 KVC对于数组而言最大的功能还是获取集合类的 count,max,min,avg,sum 这是一个很好用的功能
集合操作的keyPath整体格式如下图所示:
左边是当前对象中待操作的集合实例对象,如果是针对自身的某个属性做操作,则可以省略,”@”操作符右边的是需要操作的属性。如果操作符是count的话右边也可以省略,这时候相当于针对当前数量统计。
NSLog(@"count of book price : %@" ,[student valueForKeyPath :@"bookList.@count" ]) ;NSLog(@"count of book price : %@" ,[student valueForKeyPath :@"bookList.@count.price" ]) ;NSLog(@"min of book price : %@" ,[student valueForKeyPath :@"bookList.@min.price" ]) ;NSLog(@"avg of book price : %@" ,[student valueForKeyPath :@"bookList.@max.price" ]) ;NSLog(@"sum of book price : %@" ,[student valueForKeyPath :@"bookList.@sum.price" ]) ;NSLog(@"avg of book price : %@" ,[student valueForKeyPath :@"bookList.@avg.price" ]) ;
如果自身是一NSNumber数组的话右边的可以为self,表示针对的是自身
NSLog(@"count of book price : %@" ,[student valueForKeyPath :@"@count" ]) ;NSLog(@"count of book price : %@" ,[student valueForKeyPath :@"@count.self" ]) ;NSLog(@"min of book price : %@" ,[student valueForKeyPath :@"bookList.self" ]) ;NSLog(@"avg of book price : %@" ,[student valueForKeyPath :@"bookList.self" ]) ;NSLog(@"sum of book price : %@" ,[student valueForKeyPath :@"bookList.self" ]) ;NSLog(@"avg of book price : %@" ,[student valueForKeyPath :@"bookList.self" ]) ;
通过这个特性能够一个对象集合的所有某个特定字端的值组成一个数组
一共有两种:
@distinctUnionOfObjects @unionOfObjects
它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。例子如下:
NSArray* arrDistinct = [arrBooks valueForKeyPath:@" @distinctUnionOfObjects.price"]; NSArray* arrUnion = [arrBooks valueForKeyPath:@" @unionOfObjects.price"];
这类操作符的操作对象内对象必须是数组/集合,也就是元素为数组/集合的数组/集合:下面是一个简单例子:
IDLProductModel *productA = [[IDLProductModel alloc] init];productA.price = 99.0 ; productA.name = @"iPod" ; IDLProductModel *productB = [[IDLProductModel alloc] init];productB.price = 199000.0 ; productB.name = @"iMac" ; IDLProductModel *productC = [[IDLProductModel alloc] init];productC.price = 2990.0 ; productC.name = @"iPhone" ; IDLProductModel *productD = [[IDLProductModel alloc] init];productD.price = 1990.0 ; productD.name = @"iPhone" ; NSArray *product = @[productA, productB, productC, productD];
NSArray * distinctUnionOfArrays = [@[product, product] valueForKeyPath:@"@distinctUnionOfArrays.price" ];NSArray * unionOfArrays = [@[product, product] valueForKeyPath:@"@unionOfArrays.price" ];NSSet * setA = [NSSet setWithObjects:productA, productB, nil ];NSSet * setB = [NSSet setWithObjects:productC, productD, nil ];NSSet * set = [NSSet setWithObjects:setA, setB, nil ];NSSet * allSet = [set valueForKeyPath:@"@distinctUnionOfSets.name" ];
这种其实就是将操作对象的所有数据取出来,展开铺平为一个数组后,再对数组或者集合做操作。
5. 键值验证 KVC提供了验证Key对应的Value是否可用的方法,但是这个方法不会自动调用,必须在使用的时候手动调用:
- (BOOL) validateValue:(inoutid*) ioValue forKey:(NSString*) inKey error:(outNSError**) outError;
如下例子:
- (BOOL )validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError { NSNumber *age = *ioValue; if (age.integerValue == 10 ) { return NO ; } return YES ; } NSNumber *age = @10 ;NSError * error;NSString *key = @"age" ;BOOL isValid = [test validateValue:&age forKey:key error:&error];
6.其他用途
指定数组中每个元素的执行方法,然后将新元素放到新数组中返回
NSArray *arr = @[@"iPod" , @"iPhone" , @"iMac" , @"iPhone8 Plus" ];NSArray *uppercaseStrArr = [arr valueForKeyPath:@"uppercaseString" ];
NSArray * array = @[ @{ @"name" : @"iPod", @"price" : @99 }, @{ @"name" : @"iPhone", @"price" : @199}, @{ @"name" : @"iPhone", @"price" : @299}, @{ @"name" : @"iPhone", @"price" : @299}, ]; NSLog(@"% @", [array valueForKeyPath:@"name"]);//iPod,iPhone,iPhone,iPhone
KVO — Key Value Observer 键值观察 1. KVO 的基本使用 KVO 适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。KVO 只能对属性做出反应,不会用来对方法或者动作做出反应
为某个对象的属性添加观察者:
[_person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial |NSKeyValueObservingOptionPrior context:nil ];
回调方法
- (void) observeValueForKeyPath:(NSString *) keyPath ofObject:(id) object change:(NSDictionary<NSString *,id> *) change context:(void *) context { NSLog(@"%@对象的%@属性改变了:%@" ,object,keyPath,change) ; }
移除观察者
[self .person removeObserver: self forKeyPath: @"age" ];
这里比较重要的是option这个参数:
NSKeyValueObservingOptionNew = 0x01 change字典包括改变后的值 NSKeyValueObservingOptionOld = 0x02 change字典包括改变前的值 NSKeyValueObservingOptionInitial = 0x04 注册后立刻触发KVO通知 NSKeyValueObservingOptionPrior = 0x08 如果指定,则在每次修改属性时,会在修改通知被发送之前预先发送一条通知给观察者, 这与-willChangeValueForKey:被触发的时间是相对应的。 这样,在每次修改属性时,实际上是会发送两条通知。
2. KVO 触发规则:
通过成员变量的setter方法/或者通过KVC都可以触发KVO
直接修改成员变量是不会触发KVC的,需要手动添加KVC才能触发。如何手动触发见下面部分。
3. 设置相互关联的属性 假如有个 Person 类,类里有三个属性,fullName、firstName、lastName。这种情况如果需要观察名字的变化,就要分别添加 fullName、firstName、lastName 三次观察,非常麻烦。通过设置相互关联的属性就会在关联的属性发生变化的时候,另外的属性也受到通知。
@interface Person : NSObject @property (nonatomic , strong ) NSString *firstName;@property (nonatomic , strong ) NSString *lastName;@property (nonatomic , strong ) NSString *fullName;@end @implementation Person + (NSSet <NSString *> *)keyPathsForValuesAffectingFullName { return [NSSet setWithObjects:@"firstName" ,@"lastName" , nil ]; } @end [_person addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:nil ]; [_person setValue:@"FistName" forKey:@"fistName" ];
上面的例子中我们不论设置Person类的firstName或者lastName 都会触发 fullName 的观察。
4. 手动KVO (禁用自动KVO) @interface Target : NSObject { int age; } - (int )age; - (void )setAge:(int )theAge; @end @implementation Target - (id )init { self = [super init]; if (nil != self ) { age = 10 ; } return self ; } - (int )age { return age; } - (void )setAge:(int )theAge { [self willChangeValueForKey:@"age" ]; age = theAge; [self didChangeValueForKey:@"age" ]; } + (BOOL )automaticallyNotifiesObserversForKey:(NSString *)key { if ([key isEqualToString:@"age" ]) { return NO ; } return [super automaticallyNotifiesObserversForKey:key]; } @end
需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法
实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
5. 查看某个对象有哪些属性被监听 如果我们想获取一个对象上有哪些观察者正在监听其属性的修改,则可以查看对象的observationInfo属性
id info = object .observationInfo; NSLog(@"%@" , [info description ]) ;
6.KVO 原理: KVO 是通过 isa-swizzling 实现的,当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中做如下的几方面工作: 1. 重写基类中任何被观察属性的 setter 方法,而通过重写就获得了 KVO 需要的通知机制- (void )setName:(NSString *)newName { [self willChangeValueForKey:@"name" ]; [super setValue:newName forKey:@"name" ]; [self didChangeValueForKey:@"name" ]; } KVO 在调用存取方法之前总是调用 willChangeValueForKey:,通知系统该 keyPath 的属性值即将变更。 当改变发生后,didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更。 之后,observeValueForKey:ofObject:change:context: 也会被调用。 2. 让这个重写的类成为原来类的派生类:除了完成上面提到的第一步操作的同时派生类还重写了class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter ,从而激活键值通知机制 3. 重写派生类的dealloc 方法释放资源
扩展阅读 *KVO进阶——KVO实现探究