KVC概述
KVC全称是Key-Value-Coding,NSObject类及其子类和内建基本数据类型都可以通过KVC的方法赋值和取值,不需要通过get和set。
KVC使用
以一个保存多个字典的数组的取值为例:
NSArray *jjArray = @[@{@"name": @"aa", @"age": @(15)},
@{@"name": @"bb", @"age": @(16)},
@{@"name": @"cc", @"age": @(17)}];
// 遍历数组获取key为"name"时对应的value
NSMutableArray *nameArray1 = [NSMutableArray array];
for (NSDictionary *dict in jjArray) {
[nameArray1 addObject:dict[@"name"]];
}
NSLog(@"nameArray1: %@", nameArray1);
// KVC方法筛选key为"name"时对应的value,返回一个数组
NSArray *nameArray2 = [jjArray valueForKey:@"name"];
NSLog(@"nameArray2: %@", nameArray2);
日志输出:
nameArray1: (
aa,
bb,
cc
)
nameArray2: (
aa,
bb,
cc
)
遍历数组和KVC的方式得到了相同的结果,但明显KVC方式的代码量要远小于遍历的方式。
对于复杂的数据结构,例如自定义类,KVC也可以快速的取值。
// 定义一个Teacher类
@interface Teacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
// 定义一个Student类
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
// ViewController.m
NSMutableArray *aArray = [[NSMutableArray alloc] initWithCapacity:0];
Teacher *teacher = [[Teacher alloc] init];
// 属性赋值
teacher.name = @"teacher";
teacher.age = 30;
// KVC赋值
// [teacher setValue:@"teacher" forKey:@"name"];
// [teacher setValue:@30 forKey:@"age"];
[aArray addObject:teacher];
for (int i = 0; i < 3; i++) {
Student *student = [[Student alloc] init];
student.name = [NSString stringWithFormat:@"student%d", i];
student.age = i;
[aArray addObject:student];
}
// KVC方法筛选每个对象中key值为"name"的value
NSArray *teacherArray = [aArray valueForKey:@"name"];
// aArray保存了1个Teacher对象和3个Student对象,每个对象中都有name属性,所以teacherArray数组会有4个值。
NSLog(@"teacherArray: %@", teacherArray);
日志输出:
nameArray: (
teacher,
student0,
student1,
student2
)
对于自定义类中含有自定义类的情况,valueForKey: 方法已经无法获取到正确的值了,需要调用valueForKeyPath: 方法传入属性的路径来获取(xxx.xxx.xxx)。
// Teacher.h
@interface Teacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
// 在Teacher类中增加一个Student类型的数据属性students
@property (nonatomic, strong) NSMutableArray *students;
@end
// ViewController.m
NSMutableArray *aArray = [[NSMutableArray alloc] initWithCapacity:0];
Teacher *teacher = [[Teacher alloc] init];
teacher.name = @"teacher";
teacher.age = 30;
teacher.students = [NSMutableArray array];
[aArray addObject:teacher];
for (int i = 0; i < 3; i++) {
Student *student = [[Student alloc] init];
student.name = [NSString stringWithFormat:@"student%d", i];
student.age = i;
[teacher.students addObject:student];
}
NSArray *nameArray = [aArray valueForKey:@"name"];
NSArray *studentNameArray = [aArray valueForKeyPath:@"students.name"];
NSLog(@"nameArray: %@, studentNameArray: %@", nameArray, studentNameArray);
日志输出:
nameArray: (
teacher
),
studentNameArray: (
(
student0,
student1,
student2
)
)
在这段代码中,[aArray valueForKey:@"name"]方法只获取到了teacher对象name属性的值,而[aArray valueForKeyPath:@"students.name"]才能获取到student对象name属性的值。
KVC的实现
在网上查了一些资料,对KVC的键值查找方式、KVC的实现原理和内部机制做了详细的阐述,本文直接引用过来。
KVC键值查找
setValue:forKey:搜索方式
1、首先搜索setKey:方法。(key指成员变量名,首字母大写)
2、上面的setter方法没找到,如果类方法accessInstanceVariablesDirectly返回YES。那么按 _key,_isKey,key,iskey的顺序搜索成员名。(NSKeyValueCodingCatogery中实现的类方法,默认实现为返回YES)
3、如果没有找到成员变量,调用setValue:forUnderfinedKey:
valueForKey:的搜索方式
1、首先按getKey,key,isKey的顺序查找getter方法,找到直接调用。如果是BOOL、int等内建值类型,会做NSNumber的转换。
2、上面的getter没找到,查找countOfKey、objectInKeyAtindex、KeyAtindexes格式的方法。如果countOfKey和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的代理集合的NSArray消息方法。
3、还没找到,查找countOfKey、enumeratorOfKey、memberOfKey格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合。
4、还是没找到,如果类方法accessInstanceVariablesDirectly返回YES。那么按 _key,_isKey,key,iskey的顺序搜索成员名。
5、再没找到,调用valueForUndefinedKey。
KVC实现原理
KVC运用了isa-swizzing技术。isa-swizzing就是类型混合指针机制。KVC通过isa-swizzing实现其内部查找定位。isa指针(is kind of 的意思)指向维护分发表的对象的类,该分发表实际上包含了指向实现类中的方法的指针和其他数据。
比如说如下的一行KVC代码:
[site setValue:@"sitename" forKey:@"name"];
//会被编译器处理成
SEL sel = sel_get_uid(setValue:forKey);
IMP method = objc_msg_loopup(site->isa,sel);
method(site,sel,@"sitename",@"name");
每个类都有一张方法表,是一个hash表,值是还书指针IMP,SEL的名称就是查表时所用的键。
SEL数据类型:查找方法表时所用的键。定义成char*,实质上可以理解成int值。
IMP数据类型:他其实就是一个编译器内部实现时候的函数指针。当Objective-C编译器去处理实现一个方法的时候,就会指向一个IMP对象,这个对象是C语言表述的类型。
KVC内部机制
一个对象在调用setValue的时候进行了如下操作:
- 根据方法名找到运行方法的时候需要的环境参数
- 他会从自己的isa指针结合环境参数,找到具体的方法实现接口
- 再直接查找得来的具体的实现方法
引用地址:http://www.cnblogs.com/zy1987/p/4616063.html
KVC在SDWebImage中的应用
SDWebImage库Downloader模块中的SDWebImageDownloaderOperation类负责执行下载任务,它定义了一个属性callbackBlocks
typedef NSMutableDictionary SDCallbacksDictionary;
@property (strong, nonatomic, nonnull) NSMutableArray *callbackBlocks;
callbackBlocks是一个可变素组,其中每个元素是SDCallbacksDictionary类型的字典,用键值对的方式保存每个下载任务的progressBlock和completedBlock。progressBlock和completedBlock由外部传入,负责下载过程中和下载完成时或下载异常情况的处理。
假如想取到其中所有的progressBlock或completedBlock,一种方法是遍历callbackBlocks数组,根据key来获取value,保存到一个新的数组中。另一种快速的方式就是KVC,源代码如下:
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callbacks];
UNLOCK(self.callbacksLock);
return callbacks;
}
- (nullable NSArray *)callbacksForKey:(NSString *)key {
LOCK(self.callbacksLock);
NSMutableArray *callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
UNLOCK(self.callbacksLock);
// We need to remove [NSNull null] because there might not always be a progress block for each callback
[callbacks removeObjectIdenticalTo:[NSNull null]];
return [callbacks copy]; // strip mutability here
}
callbacksLock属性是一个信号量锁,初始值为1,表示同时期只能有一个线程来访问callbacks,保证callbacks数组在赋值和取值过程中的线程安全。
@property (strong, nonatomic, nonnull) dispatch_semaphore_t callbacksLock;
// 设置信号量为1
_callbacksLock = dispatch_semaphore_create(1);
总结
虽然KVC在效率上要优于遍历的方式,但是滥用KVC会导致异常问题的出现,假如valueForKey: 或valueForKeyPath:传入了不存在的key值,那么就会导致程序崩溃。所以在没有把握的情况下,慎用KVC!