详细文档,能够参考官方文档 KVC官方文档 developer.apple.com/library/arc… html
//根据key取值 - (id)valueForKey:(NSString *)key; //根据key设值 - (void)setValue:(nullable id)value forKey:(NSString *)key; //根据keyPath取值 - (nullable id)valueForKeyPath:(NSString *)keyPath; //根据keyPath设值 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; 复制代码
官方文档 ios
1.首先查找setter访问器方法,按照setKey, _setKey, setIsKey顺序查找,找到一个就返回,再也不继续查找. 若是找到访问器方法,开始检查参数类型。若是参数类型是指针对象类型,调用方法便可。若是value是非对象指针类型(简单数值类型或结构体),可是给定的value是nil,则会调用 setNilValueForKey 方法。setNilValueForKey 方法默认实现会抛出NSInvalidArgumentException异常致使程序崩溃,咱们能够重写它。 若是找不到访问器方法,则进行下一步2macos
2.没找到访问器方法,则会调用类方法 accessInstanceVariablesDirectly(默认返回YES),若返回YES,则按照顺序: _key , _isKey , key, isKey进行查找(找到一个不继续查找).若是找到了这样的实例变量,而且其类型是对象指针类型,先release旧对象,再赋值新对象。若是变量类型是其余类型(基本类型),则先使用NSNumber 或者 NSValue 进行转换,再进行赋值操做数组
3.若没找到访问器方法,也没有找到实例变量,则会调用 setValue:forUndefinedKey:方法. setValue:forUndefinedKey: 方法默认实现会抛出NSUnknownKeyException异常致使程序崩溃,咱们能够重写它bash
备注:这里官方文档没有说起到setIsKey方法(不排除将来会移除该方法),截止目前,该方法依然会被查找!markdown
为了方便验证设值流程,咱们不使用@property (不想要系统自动给咱们生成getter/setter访问器 以及带下划线_key成员变量,注意不会生成其余成员变量) 新建一个Person类app
#import <Foundation/Foundation.h> #import "Address.h" NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject @property (nonatomic, strong) Address * address; @end NS_ASSUME_NONNULL_END 复制代码
//Person.m实现类 #import "Person.h" @interface Person () { NSString* _name; NSString* name; NSString* _isName; NSString* isName; NSInteger age; NSPoint location; } @end @implementation Person -(void)setIsName:(NSString*)n{ NSLog(@"%s",__func__); isName = n; } -(void)_setName:(NSString*)n{ NSLog(@"%s",__func__); _name = n; } -(void)setName:(NSString*)n{ NSLog(@"%s",__func__); name = n; } - (void)setValue:(id)value forKey:(NSString *)key{ NSLog(@"%s",__func__); [super setValue:value forKey:key]; NSLog(@"%s",__func__); } + (BOOL)accessInstanceVariablesDirectly{ NSLog(@"%s",__func__); return [super accessInstanceVariablesDirectly]; } @end 复制代码
#import <Foundation/Foundation.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person* p = [[Person alloc] init]; //[p setValue:@"JayJay" forKey:@"_name"];方式会直接获取实例变量 [p setValue:@"JayJay" forKey:@"name"]; } return 0; } 复制代码
运行断点调试 oop
能够看到,优先查找到setName方法,而且给成员变量值name(可随意修改哪一个成员变量)进行设值 接着将setName访问器方法注释掉运行 ui
接着将_setName访问器方法注释掉运行 编码
接着注释掉setIsName方法,再次运行
接着将成员变量_name注释掉,再次运行
接着将成员变量_isName注释掉,再次运行
接着将成员变量name注释掉,再次运行
接着将 isName注释,再次运行 程序崩溃,在方法 setValue:forUndefinedKey: 抛出 NSUnknownKeyException异常,咱们能够重写该方法(空实现程序就不会崩溃).这里若是将全部成员变量不注释,注释掉setKey,_setKey,setIsKey这三个方法,而重写类方法 accessInstanceVariablesDirectly,让其返回NO,也会出现NSUnknownKeyException异常致使崩溃
这里防止崩溃,咱们重写setValue: forUndefinedKey 方法
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{ NSLog(@"%s",__func__); } 复制代码
官方文档:
1.查找接收者访问器(这里是getter)方法,按照 getKey,key ,isKey,_getKey , _key 顺序寻找方法,找到一个就返回,再也不继续查找 若是方法返回结果是对象指针类型,则直接返回,若是结果的类型是NSNumber支持的数值类型之一,则完成转换并返回NSNumber. 不然,将完成转换并返回NSValue(Mac OS 10.5中的新增功能:任意类型的结果都将转换为NSValues,而不只仅是NSPoint,NSRange,NSRect和NSSize)
2 . 若是没有找到简单getter方法,则匹配countOfKey方法和objectInKeyAtIndex:方法 或者 countOfKey方法和keyAtIndexes:方法 若匹配到,则动态建立一个NSArray集合代理对象,该对象响应全部NSArray方法并返回。不然,执行下一步3
备注:动态建立的NSArray代理对象会将NSArray接收到的countOfKey、objectInKeyAtIndex: 、keyAtIndexes:的消息返回给符合KVC规则的调用者.此时NSArray代理对象与上面的方法工做时,系统会将该对象当作NSArray对象,这样能够响应全部NSArray方法
3.若是没有找到NSArray的简单getter方法,或者NSArray的存取方法组,则查找有没有countOfKey、enumeratorOfKey、memberOfKey: 命名的方法(三者缺一不可)。若是找到,则动态建立一个NSSet集合代理对象,该对象响应全部NSSet方法并返回。不然,执行下一步4
备注:动态建立的NSSet代理对象会将NSSet接收到的countOfKey、enumeratorOfKey、memberOfKey: 的消息返回给符合KVC规则的调用者.此时NSSet代理对象与上面的方法工做时,系统会将该对象当作NSSet对象,这样能够响应全部NSSet方法
4.若是没有找到简单getter方法,或集合存取方法组,以及类方法accessInstanceVariablesDirectly返回YES(默认返回YES),则按照_key , _isKey , key, isKey顺序进行查找实例,若是找到对应的实例,则马上获取实例可用的值并跳转到第5步,不然,跳转到第6步.
5.若是取回的是一个指针对象,则直接返回这个结果。若是取回的是简单值类型,而且能够被NSNumber或者NSValue转换,那么转换后再返回
6.若是都没有找到,则会调用 valueForUndefinedKey:方法,会抛出NSUnknownKeyException异常崩溃,能够重写该方法。
备注:这里官方文档没有说起到_getKey (不排除将来移除可能性)方法,可是Xcode API点击进去能够在最后发现下图所示提示(Apple从Mac OS 10.3开始不建议使用下划线开头的KVC方法),实际截至目前仍然会对此方法进行查找
#import <Foundation/Foundation.h> #import "Address.h" NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject @property (nonatomic, strong) Address * address; @end NS_ASSUME_NONNULL_END 复制代码
#import "Person.h" @interface Person () { NSString* _name; NSString* name; NSString* _isName; NSString* isName; NSInteger age; NSPoint location; } @end @implementation Person - (instancetype)init { self = [super init]; if (self) { _name = @"_JayJay"; _isName = @"_isJayJay"; name = @"JayJay"; isName = @"isJayJay"; } return self; } -(NSInteger)countOfName{ NSLog(@"%s",__func__); return 3; } //具体能够参考NSArray里面 API方法 -(NSString*)objectInNameAtIndex:(NSInteger)index{ NSLog(@"%s",__func__); return @"objectInNameAtIndex"; } - (NSArray<NSString*> *)nameAtIndexes:(NSIndexSet *)indexes{ NSLog(@"%s",__func__); return [@[@"111",@"222",@"333"] objectsAtIndexes:indexes]; } //具体能够参考NSSet里面 API方法 -(NSEnumerator<NSString*> *)enumeratorOfName{ NSLog(@"%s",__func__); return [@[@"111",@"222",@"333"] objectEnumerator]; } - (nullable NSString*)memberOfName:(NSString*)object{ NSLog(@"%s",__func__); return @"memberOfName"; } -(NSString*)_name{ NSLog(@"%s",__func__); return _name; } -(NSString*)_getName{ NSLog(@"%s",__func__); return _isName; } -(NSString*)isName{ NSLog(@"%s",__func__); return isName; } -(NSString*)name{ NSLog(@"%s",__func__); return name; } -(NSString*)getName{ NSLog(@"%s",__func__); return _name; } - (id)valueForKey:(NSString *)key{ NSLog(@"%s",__func__); id temp = [super valueForKey:key]; NSLog(@"%s",__func__); return temp; } + (BOOL)accessInstanceVariablesDirectly{ NSLog(@"%s",__func__); return [super accessInstanceVariablesDirectly]; } @end 复制代码
#import <Foundation/Foundation.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person* p = [[Person alloc] init]; //[p valueForKey:@"_name"];方式会直接获取实例变量 id result = [p valueForKey:@"name"]; NSLog(@"result = %@",result); } return 0; } 复制代码
断点调试,首先查找getName方法
注释掉objectInNameAtIndex:方法,再次运行,会发现匹配countOfName和nameAtIndexes:方法
注释掉objectInNameAtIndex:和 nameAtIndexes:方法,再次运行,会发现匹配countOfName,enumeratorOfName,memberOfName:方法(虽然没看到调用它,系统内部去处理了,注释掉这个方法,则会致使匹配失败)
注释掉全部简单getter方法,全部集合存取方法,则会去调用类方法accessInstanceVariablesDirectly(默认返回YES),若返回YES,则按照_name , _isName , name , isName顺序取查找,若找到,直接获取值返回 (相似上面setter过程,这里就再也不赘述了)
若是上述全部简单getter方法,全部集合存取方法以及实例变量都没有找到,则会调用valueForUndefinedKey:方法,抛出NSUnknownKeyException异常崩溃,能够重写该方法
-(id)valueForUndefinedKey:(NSString *)key{ NSLog(@"%s",__func__); return NULL; } 复制代码
/* 默认返回YES,表示若是没有找到setter或者getter访问器方法 就会按照_key,_iskey,key,iskey的顺序查找实例变量 若是返回NO,则跳过查找实例变量 */ + (BOOL)accessInstanceVariablesDirectly; /* 对属性值正确性进行验证,它能够用来检查set的值是否正确, 同时为不正确的值作一个替换值或者拒绝设置新值并返回错误缘由 */ - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; /* 若是属性是一个NSMutableArray,那么能够用这个方法来返回一个集合NSMutableArray 对集合操做的API,里面还有一系列这样的API */ - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; /* 若是没法搜索到任何和key有关的getter方法或者实例变量 则会调用这个方法,默认会抛出异常 */ - (nullable id)valueForUndefinedKey:(NSString *)key; /* 若是没法搜索到任何和key有关的setter方法或者实例变量 则会调用这个方法,默认会抛出异常 */ - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; /* 若是在setValue过程当中,给非对象属性赋值nil,就会调用此方法,默认会抛出异常 */ - (void)setNilValueForKey:(NSString *)key; /* 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model模型转字典 */ - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys; /* 输入一个字典,给调用者对象各个成员变量赋值,用于将字典转Model模型 */ - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues; 复制代码
还有访问集合属性的KVC方法:
想象这样一种情形:一个类的成员变量多是自定义类型或者更复杂类型,咱们可使用KVC来操做这个属性,而后再次使用KVC对自定义的属性或者复杂类型进行操做,显然比较繁琐.因而苹果KVC给咱们提供了一个解决方案就是keyPath,就是按路径递归调用(使用“.”号分割)寻找key 来看具体代码:
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Address : NSObject @property (nonatomic, copy) NSString* street; @end NS_ASSUME_NONNULL_END #import "Address.h" @implementation Address @end 复制代码
#import <Foundation/Foundation.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; p.address = [[Address alloc] init]; [p setValue:@"中南路街道" forKeyPath:@"address.street"]; NSLog(@"address.street = %@", [p valueForKeyPath:@"address.street"]); } return 0; } 复制代码
kvc中主要会出现两种异常:
这里NSUnknownKeyException异常上面已经提到过,下面来看NilKey异常
#import <Foundation/Foundation.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person* p = [[Person alloc] init]; //这里age实例变量是NSInteger 简单数值类型,赋值nil,KVC默认会抛出异常而崩溃 [p setValue:nil forKey:@"age"]; } return 0; } 复制代码
能够重写Person类的setNilValueForKey方法
- (void)setNilValueForKey:(NSString *)key{ NSLog(@"%s",__func__); } 复制代码
咱们仔细查看-(id)valueForKey:(NSString *)key 知道,方法老是返回一个对象,可是实际应用中彻底可能返回的不是对象。那么怎么去处理? 此外- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法中传入的value也必须是对象类型,那又该怎么去处理?固然说的这些Apple早就给咱们处理好了。具体就是:对于设值操做,须要咱们本身对非对象数据类型(这里主要是值类型或者结构体类型)进使用NSNumber 或者 NSValue来包装.而对于取值操做,Apple已经使用NSNumber 或者 NSValue给咱们封装好了非对象数据类型,返回的就是NSNumber 或者 NSValue对象
NSNumber支持的值类型有哪些,具体能够参看Apple 提供的API:
@interface NSNumber (NSNumberCreation) + (NSNumber *)numberWithChar:(char)value; + (NSNumber *)numberWithUnsignedChar:(unsigned char)value; + (NSNumber *)numberWithShort:(short)value; + (NSNumber *)numberWithUnsignedShort:(unsigned short)value; + (NSNumber *)numberWithInt:(int)value; + (NSNumber *)numberWithUnsignedInt:(unsigned int)value; + (NSNumber *)numberWithLong:(long)value; + (NSNumber *)numberWithUnsignedLong:(unsigned long)value; + (NSNumber *)numberWithLongLong:(long long)value; + (NSNumber *)numberWithUnsignedLongLong:(unsigned long long)value; + (NSNumber *)numberWithFloat:(float)value; + (NSNumber *)numberWithDouble:(double)value; + (NSNumber *)numberWithBool:(BOOL)value; + (NSNumber *)numberWithInteger:(NSInteger)value API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); + (NSNumber *)numberWithUnsignedInteger:(NSUInteger)value API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); @end 复制代码
NSValue支持的值类型(macOS 和 iOS有所不一样,这里展现的是macOS,具体能够参看API):
+ (NSValue *)valueWithPoint:(NSPoint)point;
+ (NSValue *)valueWithSize:(NSSize)size;
+ (NSValue *)valueWithRect:(NSRect)rect;
+ (NSValue *)valueWithEdgeInsets:(NSEdgeInsets)insets API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
+ (NSValue *)valueWithRange:(NSRange)range;
复制代码
来看点代码:
#import <Foundation/Foundation.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; //传入的value,是简单数值类型,须要咱们本身使用NSNumber来封装(也能够:[p setValue:@20] forKey:@"age"]) [p setValue:[NSNumber numberWithInt:20] forKey:@"age"]; //传入的value,是结构体类型,须要咱们本身使用NSValue来封装 [p setValue:[NSValue valueWithPoint:CGPointMake(100, 100)] forKey:@"location"]; //取值操做,这里返回的并非NSInteger类型,而是NSNumber类型 id age = [p valueForKey:@"age"]; //取值操做,这里返回的并非NSPoint类型,而是NSValue类型 id location = [p valueForKey:@"location"]; } return 0; } 复制代码
KVC提供了对给定的key来对对应的value进行验证,以确保value是否可用。默认状况下KVC中不会自动调用,而CoreData在保存托管上下文时会自动验证(能够查看Core Data文档developer.apple.com/library/arc… )。 此外在macOS 中 ,Cocoa Bindings 也会建议您应该自动验证(能够查看 Cocoa Bindings相关文档 developer.apple.com/library/arc… )
Person类重写下面方法:
-(BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{ NSLog(@"%s",__func__); NSString* value = *ioValue; if ([value isEqualToString:@"JayJay"]) { return NO; } return [super validateValue:ioValue forKey:inKey error:outError]; } 复制代码
备注:该方法会在内部调用(BOOL)validateKey: error: 也就意味着咱们能够不须要上面方法,而直接使用它进行单独验证!
来看代码:
#import <Foundation/Foundation.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person *p = [[Person alloc] init]; NSString*key = @"name"; NSString*value = @"JayJay"; NSError* error; BOOL valid = [p validateValue:&value forKey:key error:&error]; if (valid) { NSLog(@"键值匹配"); [p setValue:value forKey:key]; }else{ NSLog(@"键值不匹配"); } } return 0; } 复制代码
再来看如何单独验证 注释掉
Person类中添加方法:
- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{ NSLog(@"%s",__func__); if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 10)) { if (outError != NULL) { *outError = [NSError errorWithDomain:@"PersonErrorDomain" code:110 userInfo:@{ NSLocalizedDescriptionKey : @"姓名过短了!!!" }]; } return NO; } return YES; } 复制代码
集合运算符分三类:
聚合运算符(Collection operator)有5种: @avg, @count , @max , @min ,@sum
注意:这里聚合运算符只对NSArray,NSSet操做不包括NSDictionary(NSDictionary 仅仅支持@count操做)
来看点例子:
#import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic, copy) NSString* name; @property (nonatomic, assign) NSInteger age; @end @implementation Person @end int main(int argc, const char * argv[]) { @autoreleasepool { NSMutableArray* pArr = [NSMutableArray array]; for (int i= 0; i< 5; i++) { Person* p = [[Person alloc ] init]; p.age = i + 20; p.name = [NSString stringWithFormat:@"JayJay%d",i]; [pArr addObject:p]; } //@count特殊 NSNumber* count = [pArr valueForKeyPath:@"@count"]; NSLog(@"count: %d",count.intValue); NSNumber* sum = [pArr valueForKeyPath:@"@sum.age"]; NSLog(@"sum: %d",sum.intValue); NSNumber* avg = [pArr valueForKeyPath:@"@avg.age"]; NSLog(@"avg: %d",avg.intValue); NSNumber* min = [pArr valueForKeyPath:@"@min.age"]; NSLog(@"min: %d",min.intValue); NSNumber* max = [pArr valueForKeyPath:@"@max.age"]; NSLog(@"max: %d",max.intValue); } return 0; } 复制代码
数组运算有以下两种:
@distinctUnionOfObjects 返回一个去重数组 @unionOfObjects 返回一个不去重数组
注意:若是有一个对象为nil,就会形成异常崩溃
例子:
#import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic, copy) NSString* name; @property (nonatomic, assign) NSInteger age; @end @implementation Person @end int main(int argc, const char * argv[]) { @autoreleasepool { NSMutableArray* pArr = [NSMutableArray array]; for (int i= 1; i <= 5; i++) { Person* p = [[Person alloc ] init]; p.age = 20 + i%2; p.name = [NSString stringWithFormat:@"JayJay%d",i]; [pArr addObject:p]; } NSArray *distinctAgePersons = [pArr valueForKeyPath:@"@distinctUnionOfObjects.age"]; NSArray *persons = [pArr valueForKeyPath:@"@unionOfObjects.age"]; } return 0; } 复制代码
这里操做属性是age,简单数值int类型,返回结果数组里面对应的就是NSNumber类型
有三种操做符: @distinctUnionOfArrays @unionOfArrays @distinctUnionOfSets
代码:
#import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic, copy) NSString* name; @property (nonatomic, assign) NSInteger age; @end @implementation Person @end int main(int argc, const char * argv[]) { @autoreleasepool { NSMutableArray* pArr = [NSMutableArray array]; for (int i= 1; i <= 5; i++) { Person* p = [[Person alloc ] init]; p.age = 20 + i; p.name = [NSString stringWithFormat:@"JayJay%d",i]; [pArr addObject:p]; } NSMutableArray* moreArr = [NSMutableArray array]; for (int i= 1; i <= 5; i++) { Person* p = [[Person alloc ] init]; p.age = 22 + i; p.name = [NSString stringWithFormat:@"More%d",i]; [moreArr addObject:p]; } NSArray* arrayOfArrays = @[pArr, moreArr]; NSArray *collectedDistinctPersons = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.age"]; NSArray *collectedPersons = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.age"]; } return 0; } 复制代码
KVC很方便来处理字典和模型之间的转换,这里须要注意一点的就是要处理好KVC异常(上面已经提到过),来加强程序健壮性
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys; - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues; 复制代码
示例代码:
#import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic, copy) NSString* name; @property (nonatomic, copy) NSString* sex; @property (nonatomic, strong) NSArray* languages; @property (nonatomic, assign) NSInteger age; @end @implementation Person @end int main(int argc, const char * argv[]) { @autoreleasepool { NSDictionary * dic = @{ @"name":@"JayJay", @"sex":@"男", @"age":@20, @"languages":@[@"C++",@"Objective-c",@"Swift",@"Dart",@"Python"] }; Person* p = [[Person alloc] init]; [p setValuesForKeysWithDictionary:dic]; NSArray* keys = [dic allKeys]; NSDictionary * dict = [p dictionaryWithValuesForKeys:keys]; NSLog(@"dict = %@" ,dict); } return 0; } 复制代码
写在最后: 在使用KVC过程当中,因为传入的key或者keyPath都是字符串,手动设置或者修改后很容易出错,从而容易Crash.这里咱们能够采用iOS反射机制尽可能避免这个问题.具体作法就是:经过@selector()获取到方法的SEL,而后经过NSStringFromSelector()将SEL反射为字符串,这样在@selector()中传入方法名的时候,编译器会有合法性检查,若是方法不存在或未实现,则会报黄色警告,这样就能够减小出错的几率