1、什么是KVC?
KVC(Key-value coding)键值编码,是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。
2、KVC相关的方法?
主要的方法:
1 | //直接通过Key来取值 |
其他的方法:
1 | //默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索 |
3、实现原理是什么?
当调用setValue:属性值 forKey:@”name“的代码时,底层的执行机制如下:
1、程序优先调用set
2、如果没有找到setName:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法,不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为
3、如果该类即没有set
4、和上面一样,如果该类即没有set
5、如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。
当调用valueForKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:
1、首先按get
2、如果上面的getter没有找到,KVC则会查找countOf
3、如果上面的方法没有找到,那么会同时查找countOf
可能上面的两条查找方案不好理解,简单来说就是如果你在自己的类自定义了KVC的实现,并且实现了上面的方法,那么你可以将返回的对象当数组(NSArray)用
4、如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_
5、还没有找到的话,调用valueForUndefinedKey:
大致实现原理如下:
1 | @interface NSObject(MYKVC) |
4、KVC 如何处理异常?
KVC中最常见的异常就是不小心使用了错误的key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常。
通常在用KVC操作Model时,抛出异常的那两个方法是需要重写的。虽然一般很小出现传递了错误的Key值这种情况,但是如果不小心出现了,直接抛出异常让APP崩溃显然是不合理的。一般在这里直接让这个key打印出来即可,或者有些特殊情况需要特殊处理。通常情况下,KVC不允许你要在调用setValue:属性值 forKey:@”name“(或者keyPath)时对非对象传递一个nil的值。很简单,因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。
1 | - (nullable id)valueForUndefinedKey:(NSString *)key; |
5、KVC如何处理非对象和自定义对象 ?
不是每一个方法都返回对象,但是valueForKey:总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber或者NSValue对象。这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开发者需要手动转换成原来的类型。尽管valueForKey:会自动将值类型封装成对象,但是setValue:forKey:却不行。你必须手动将值类型转换成NSNumber或者NSValue类型,才能传递过去。
对于自定义对象,KVC也会正确地设值和取值。因为传递进去和取出来的都是id类型,所以需要开发者自己担保类型的正确性,运行时Objective-C在发送消息时会检查类型,如果错误会直接抛出异常。
6、KVC处理容器类?
对象的属性可以是一对一的,也可以是一对多的。一对多的属性要么是有序的(数组),要么是无序的(集合)。
不可变的有序容器属性(NSArray)和无序容器属性(NSSet)一般可以使用valueForKey:来获取。比如有一个叫items的NSArray属性,你可以用valurForKey:@”items”来获取这个属性。前面valueForKey:的key搜索模式中,我们发现其实KVC使用了一种更灵活的方式来管理容器类。苹果的官方文档也推荐我们实现这些这些特殊的访问器。
而当对象的属性是可变的容器时,对于有序的容器,可以用下面的方法:
1 |
|
//是指输入一组key,返回这组key对应的属性,再组成一个字典。
- (NSDictionary
)dictionaryWithValuesForKeys:(NSArray<NSString > *)keys;
//是用来修改Model中对应key的属性,字典里传属性名和值 (void)setValuesForKeysWithDictionary:(NSDictionary
*)keyedValues; 1
2
3
4
5
# 8、KVC校验值得正确性?
KVC提供了属性值,用来验证key对应的Value是否可用的方法(BOOL)validateValue:(inout id nullable * nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
1
2
这个方法的默认实现是去探索类里面是否有一个这样的方法:`-(BOOL)validate<Key>:error:`如果有这个方法,就调用这个方法来返回,没有的话就直接返回YES
@implementation Address
-(BOOL)validateCountry:(id )value error:(out NSError _Nullable __autoreleasing )outError{ //在implementation里面加这个方法,它会验证是否设了非法的value
NSString country = *value;
country = country.capitalizedString;
if ([country isEqualToString:@”Japan”]) {
return NO; //如果国家是日本,就返回NO,这里省略了错误提示,
}
return YES;
}
@end1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
如上面的代码,当开发者需要验证能不能用KVC设定某个值时,可以调用validateValue: forKey:这个方法来验证,如果这个类的开发者实现了-(BOOL)validate<Key>:error:这个方法,那么KVC就会直接调用这个方法来返回,如果没有,就直接返回YES,注意,KVC在设值时不会主动去做验证,需要开发者手动去验证。所以即使你在类里面写了验证方法,但是KVC因为不会去主动验证,所以还是能够设值成功。
# 9、具体使用场景有哪些?
1、动态地取值和设值
利用KVC动态的取值和设值是最基本的用途了
2、用KVC来访问和修改私有变量
对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的。
3、Model和字典转换
充分地运用KVC和Objc的runtime组合的技巧,可以实现很多功能,比如给Model实现统一的description用于打印model
4、修改一些控件的内部属性
很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些控件的API,这样我们就无法正常地访问和修改这些控件的样式。而KVC在大多数情况可下可以解决这个问题。最常用的就是个性化UITextField中的placeHolderText相关属性,颜色啥的。
当日具体属性名称可以通过Runtime相关方法来获取
5、更方便的操作集合
Apple对KVC的valueForKey:方法作了一些特殊的实现,比如说NSArray和NSSet这样的容器类就实现了这些方法。所以可以用KVC很方便地操作集合
比如 求和、首字母变大写等等。。
用KVC实现高阶消息传递
当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。
NSArray arrStr = @[@”english”,@”franch”,@”chinese”];
NSArray arrCapStr = [arrStr valueForKey:@”capitalizedString”];
for (NSString str in arrCapStr) {
NSLog(@”%@”,str);
}
NSArray arrCapStrLength = [arrStr valueForKeyPath:@”capitalizedString.length”];
for (NSNumber* length in arrCapStrLength) {
NSLog(@”%ld”,(long)length.integerValue);
}
打印结果
2016-04-20 16:29:14.239 KVCDemo[1356:118667] English
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Franch
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Chinese
2016-04-20 16:29:14.240 KVCDemo[1356:118667] 7
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 6
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 71
2
3
4
5
6
7
8方法capitalizedString被传递到NSArray中的每一项,这样,NSArray的每一员都会执行capitalizedString并返回一个包含结果的新的NSArray。从打印结果可以看出,所有String都成功以转成了大写。
同样如果要执行多个方法也可以用valueForKeyPath:方法。它先会对每一个成员调用 capitalizedString方法,然后再调用length,因为lenth方法返回是一个数字,所以返回结果以NSNumber的形式保存在新数组里
用KVC中的函数操作集合
KVC同时还提供了很复杂的函数,主要有下面这些
①简单集合运算符
简单集合运算符共有 `@avg, @count , @max , @min ,@sum5` 种,目前还不支持自定义。
@interface Book : NSObject
@property (nonatomic,copy) NSString* name;
@property (nonatomic,assign) CGFloat price;
@end
@implementation Book
@end
Book book1 = [Book new];
book1.name = @”The Great 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 arrBooks = @[book1,book2,book3,book4];
NSNumber sum = [arrBooks valueForKeyPath:@”@sum.price”];
NSLog(@”sum:%f”,sum.floatValue);
NSNumber avg = [arrBooks valueForKeyPath:@”@avg.price”];
NSLog(@”avg:%f”,avg.floatValue);
NSNumber count = [arrBooks valueForKeyPath:@”@count”];
NSLog(@”count:%f”,count.floatValue);
NSNumber min = [arrBooks valueForKeyPath:@”@min.price”];
NSLog(@”min:%f”,min.floatValue);
NSNumber max = [arrBooks valueForKeyPath:@”@max.price”];
NSLog(@”max:%f”,max.floatValue);
打印结果
2016-04-20 16:45:54.696 KVCDemo[1484:127089] sum:256.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] avg:64.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] count:4.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] min:12.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] max:111.0000001
2
3
4
5
6
7
②对象运算符
比集合运算符稍微复杂,能以数组的方式返回指定的内容,一共有两种:
@distinctUnionOfObjects
@unionOfObjects
它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。
用法如下:
NSLog(@”distinctUnionOfObjects”);
NSArray arrDistinct = [arrBooks valueForKeyPath:@”@distinctUnionOfObjects.price”];
for (NSNumber price in arrDistinct) {
NSLog(@”%f”,price.floatValue);
}
NSLog(@”unionOfObjects”);
NSArray arrUnion = [arrBooks valueForKeyPath:@”@unionOfObjects.price”];
for (NSNumber price in arrUnion) {
NSLog(@”%f”,price.floatValue);
}
2016-04-20 16:47:34.490 KVCDemo[1522:128840] distinctUnionOfObjects
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] unionOfObjects
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000`
前者会将重复的价格去除后返回所有价格,后者直接返回所有的图书价格。(因为只返回价格,没有返回图书,感觉用处不大。)
③Array和Set操作符
这种情况更复杂了,说的是集合中包含集合的情况,我们执行了如下的一段代码:
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets
@distinctUnionOfArrays:该操作会返回一个数组,这个数组包含不同的对象,不同的对象是在从关键路径到操作器右边的被指定的属性里
@unionOfArrays 该操作会返回一个数组,这个数组包含的对象是在从关键路径到操作器右边的被指定的属性里和@distinctUnionOfArrays不一样,重复的对象不会被移除
@distinctUnionOfSets 和@distinctUnionOfArrays类似。因为Set本身就不支持重复。