键值编码
在iOS开发中,允许通过key名直接访问或者赋值对象的属性,而不用通过存取方法。这样就可以在运行时动态的访问和修改对象的属性,而不是在编译时确定。
定义与使用
KVC的定义是通过对NSObject的扩展来实现的,有个显示的NSKeyValueCoding类别名,对于所有继承了NSObject的类型,都能使用KVC。
寻找key
我们可以在Xcode里面,打开NSKeyValueCoding.h文件,里面有关于KVC的完整介绍。
valueForKey:
- 首先按照-get<Key>, -<key>, or -is<Key>的顺序来查找,如果找到的值是一个object类型就直接返回,如果是一个数据类型就会转换成NSNumber对象返回,如果是结构体类型(不仅限于:NSPoint,NSRange,NSRect,NSSize),就转换成NSValues返回
- 如果上面没有找到,就会开始查找下面几个方法-countOf<Key> 、 -indexIn<Key>OfObject: 、-objectIn<Key>AtIndex: 、 -<key>AtIndexes: 只要-countOf<Key>和-indexIn<Key>OfObject:和后面两个至少一个方法被发现了,就会返回一个可以相应所有NSOrderedSet方法的集合代理对象。所有的NSOrderedSet消息都会被发送到这个对象上,并且将会以组合的形式调用-countOf<Key>, -indexIn<Key>OfObject:, -objectIn<Key>AtIndex:, and -<key>AtIndexes:这几个方法,发送到-valueForKey:上。如果对象的类实现了一个可选的函数-get<Key>:range:那么也会被调用(提高性能)。
- 查找-countOf<Key> and -objectIn<Key>AtIndex: (匹配NSArray类),-<key>AtIndexes:(匹配-[NSArray objectsAtIndexes:])。count方法和另外两个中的至少一个被发现,返回一个响应所有NSArray方法的集合协议对象。每一个NSArray消息都有该对象响应,并以-countOf<Key>, -objectIn<Key>AtIndex:, and -<key>AtIndexes:一些组合(组合规则如前面所说)的形式被发送到原始接受者的valueForKey上。如果这个接受者的类实现了-get<Key>:range: 那么会调用,可以有更好的性能。
- 继续没有找到的话,就开始匹配-countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>这三个方法,如果这个三个方法都找到的话,就返回一个响应所有NSSet方法的集合协议对象。每一个NSSet消息都会发送给集合协议对象,然后该对象就会以-countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>三者组合的形式响应。
- 还没有找到的话,就检查+accessInstanceVariablesDirectly属性,如果返回YES,就按_<key>, _is<Key>, <key>, or is<Key>查找,找到了就直接返回,一些属性会以NSNumber或NSValue 的形式返回如前面所说。
- 上面条件都不满足,就调用-valueForUndefinedKey:我们可以重写这个方法。默认实现是抛一个NSUndefinedKeyException异常
setValue:
setValue概括起来比较简单,分为下面三步
- 查找-set<Key>:方法,如果参数类型不是一个指针类型,但是接受了一个nil值,那么就会走到-setNilValueForKey: 这个方法,默认抛NSInvalidArgumentException异常,可以重写它。数值类型也会被转为相应的对象类型(NSNumber,NSValue)
- 没有找到的话,就会检查+accessInstanceVariablesDirectly,返回YES,就开始查找_<key>, _is<Key>, <key>, or is<Key>。如果这个变量是一个对象类型,setValue的的时候,新值会被持有,旧值会被释放。
- 返回NO,就触发-setValue:forUndefinedKey:方法,逻辑和上面一致
KeyPath
如果类的成员变量是另一个类或其它的复杂数据类型,就可以使用KeyPath。 有一个address类
@interface Address : NSObject
@property (nonatomic, copy) NSString *country;
@end
另一个people类
@interface People : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) KVCAddress *address;
@property (nonatomic, assign) NSInteger age;
@end
我们可以用KeyPath来设置people实例的address中的country属性
Address *address = [Address new];
address.country = @“中国”;
People *people = [People new];
people.address = address;
NSLog(@“%@|%@“, people.address.country, [people valueForKeyPath:@“address.country”]);
[people setValue:@“日本” forKeyPath:@“address.country”];
NSLog(@“%@|%@“, people.address.country, [people valueForKeyPath:@“address.country”]);
打印结果
2020-03-23 12:03:48.686338+0800 AwesomeOC[10376:165500] 中国|中国
2020-03-23 12:03:48.687086+0800 AwesomeOC[10376:165500] 日本|日本
KVC与容器类
mutableArrayValueForKey:
- 搜索-insertObject:in<Key>AtIndex: and -removeObjectFrom<Key>AtIndex: -insert<Key>:atIndexes: and -remove<Key>AtIndexes: 如果至少找到一个insert方法和一个remove方法,返回一个可以响应NSMutableArray所有方法代理集合,给这个代理结合发送NSMutableArray的方法,以-insertObject:in<Key>AtIndex:, -removeObjectFrom<Key>AtIndex:, -insert<Key>:atIndexes:, and -remove<Key>AtIndexes:组合的形式调用。
- 搜索-set<Key>: 如果找到了,每一个发送给代理集合的NSMutableArray方法都会调用-set<Key>:
- 没找到就检查+accessInstanceVariablesDirectly属性,返回YES,就搜索_<key> or <key>,找打了,那么发送给NSMutableArray的方法就直接给这个成员变量处理。
- 还是找不到,就forUndefinedKey了。
另外还有 mutableOrderedSetValueForKey,mutableSetValueForKey 关于有序容器和无序容器的方法,这里就不写了,直接在NSKeyValueCoding.h查看即可,思路都是一致的。 它们也有对应的KeyPath版本。
KVC与字典
字典使用KVC,valueForKey:内部就是返回objectForKey: 那么使用KeyPath来访问多层嵌套字典,这是一个比较方便的操作。
- dictionaryWithValuesForKeys 提供一组key,返回这组key对应的属性,再组成一个字典
- setValuesForKeysWithDictionary 修改model中对应的key的属性
KVC验证
用来验证key对应的value是否可用
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
默认返回YES,如果重写了,就调用这个方法返回。
-(BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError {
NSString *country = *ioValue;
country = country.capitalizedString;
if ([country isEqualToString:@“Japan”]) {
return NO;
}
return YES;
}
Address *address = [Address new];
NSError *error;
id value = @“japan”;
NSString *key = @“country”;
BOOL result = [address validateValue:&value forKey:key error:&error];
if (result) {
NSLog(@“键值匹配”);
[address setValue:value forKey:key];
} else {
NSLog(@“键值不匹配”);
}
如果我在设定某个值前需要验证一下,那么就可以重写这个方法,但是要注意,KVC在setValue时并不会主动调用这个验证函数,这里需要我们手动调用才行。
手动实现KVC
- (void)setMyValue:(id)value forKey:(NSString *)key {
if (key == nil || key.length == 0) {
NSLog(@“Key不能为空”);
return;
}
if ([value isKindOfClass:[NSNull class]]) {
NSLog(@“value不能为空”);
[self setNilValueForKey:key];
return;
}
if (![value isKindOfClass:[NSObject class]]) {
@throw @“must be a NSObject type”;
return;
}
NSString *funcName = [NSString stringWithFormat:@“set%@“, key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
[self performSelector:NSSelectorFromString(funcName) withObject:value];
return;
}
unsigned int count;
BOOL flag = false;
Ivar *vars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar var = vars[i];
NSString *keyName = [NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding];
NSLog(@“keyName: %@“, keyName);
keyName = [keyName substringFromIndex:1];
if ([keyName isEqualToString:[NSString stringWithFormat:@“_%@“, key]]) {
flag = true;
object_setIvar(self, var, value);
break;
}
if ([keyName isEqualToString:key]) {
flag = true;
object_setIvar(self, var, value);
}
}
if (!flag) {
[self setValue:value forUndefinedKey:key];
}
}
- (id)myValueForKey:(NSString *)key {
if (key == nil || key.length == 0) {
return nil;
}
NSString *funcName = [NSString stringWithFormat:@“get%@:”, key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
return [self performSelector:NSSelectorFromString(funcName)];
}
unsigned int count;
BOOL flag = false;
Ivar *vars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar var = vars[i];
NSString *keyName = [NSString stringWithFormat:@“_%@“, key];
NSString *varName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
if ([varName isEqualToString:keyName]) {
flag = true;
return object_getIvar(self, var);
}
if ([varName isEqualToString:key]) {
flag = true;
return object_getIvar(self, var);
}
}
if (!flag) {
[self valueForUndefinedKey:key];
}
return nil;
}
KVC的使用
动态地取值、设值
访问和修改私有变量。
之前项目里面,需要给一个textView添加placeholder,有一种做法就是通过获取私有变量来实现的。但是在后来的iOS版本中,这种做法会报错,猜测是苹果在新的版本中移除了这个私有变量。
Model和字典转换
这个相信是最常见的,在获取网络数据转换model的时候经常用到
操作集合
这种方式会达到一种高阶函数的效果
NSArray *langArr = @[@“english”, @“franch”, @“chinese”];
NSArray *capLangArr = [langArr valueForKey:@“capitalizedString”];
NSLog(@“%@“, capLangArr);
NSArray *langLenArr = [langArr valueForKeyPath:@“capitalizedString.length”];
NSLog(@“%@“, langLenArr);
输出
2020-03-23 16:12:43.690950+0800 AwesomeOC[18336:330583] (
English,
Franch,
Chinese
)
2020-03-23 16:12:43.691636+0800 AwesomeOC[18336:330583] (
7,
6,
7
)
函数操作集合
简单集合运算符: @avg, @count, @max, @min, @sum
Book *book1 = [Book new];
book1.name = @“The Grate Gastby”;
book1.price = 22;
Book *book2 = [Book new];
book2.name = @“Time History”;
book2.price = 12;
Book *book3 = [Book new];
book3.name = @“Wrong Hole”;
book3.price = 111;
Book *book4 = [Book new];
book4.name = @“Wrong Hole”;
book4.price = 111;
NSArray *books = @[book1, book2, book3, book4];
NSNumber *sum = [books valueForKeyPath:@“@sum.price”];
NSLog(@“sum: %f”, sum.doubleValue);
NSNumber *avg = [books valueForKeyPath:@“@avg.price”];
NSLog(@“avg: %f”, avg.doubleValue);
NSNumber *count = [books valueForKeyPath:@“@count”];
NSLog(@“count: %ld”, count.integerValue);
NSNumber *min = [books valueForKeyPath:@“@min.price”];
NSLog(@“min: %f”, min.doubleValue);
NSNumber *max = [books valueForKeyPath:@“@max.price”];
NSLog(@“max: %f”, max.doubleValue);
输出
2020-03-23 16:21:35.262880+0800 AwesomeOC[18701:337719] sum: 256.000000
2020-03-23 16:21:35.263241+0800 AwesomeOC[18701:337719] avg: 64.000000
2020-03-23 16:21:35.263456+0800 AwesomeOC[18701:337719] count: 4
2020-03-23 16:21:35.263639+0800 AwesomeOC[18701:337719] min: 12.000000
2020-03-23 16:21:35.263791+0800 AwesomeOC[18701:337719] max: 111.000000
对象运算符: @distinctUnionOfObjects,@unionOfObjects
前者返回去重以后的结果,后者返回全部,这里不再细说。
总结
关于KVC的了解,其实主要是看官方文档就行,其次就是一些用法,以前对于KVC就仅仅局限在model转换这里,要么就是获取私有变量这些,这次学习下来还是很有收获的。