之前简单得讲过一些KVC的用法,但是并不能深入理解KVC
内部实现及其原理,下面主要讲下KVC的底层原理。
取值 valueForKey:
在使用KVC
取值的时候,使用valueForKey:
方法,该方法会返回一个id
类型的对象,那么它的内部会怎么处理的呢?现在我们使用该方法:
Teacher *teacher = [[Teacher alloc] init];
NSString *name = [teacher valueForKey:@"name"];
NSLog(@"%@",name);
分析:当使用KVC获取成员变量的值时,其内部会首先去找getter
方法- (NSString *)getName
,如果有该方法直接调用;如果没有就去找getter
方法- (NSString *)name
,如果有直接调用;如果没有就去找getter方法- (NSString *)getIsName
,如果有直接调用;如果没有就去找getter方法:- (NSString *)isName
,如果有直接调用;如果没有这里就会出现转折,程序不会再去找getter
方法,而是去找下面这两个方法:
// 数组元素数
- (NSInteger)countOfName {
return 5;
}
// 数组内容
- (id)objectInNameAtIndex:(NSUInteger)index {
if (index == 0) {
return @"Michael";
}
return @"Tom";
}
如果找到了这个两个方法,就会返回一个数组类型:
这里的场景虽然还没有遇到过,但是KVC
内部确实是在没有访问到getter
方法的时候,会访问这个两个方法,如果有直接返回;如果没有就开始访问成员变,在访问成员变量之前会有个判断方法:+ (BOOL)accessInstanceVariablesDirectly
,该方法默认是YES
,如果重写该方法并返回NO
,那么将失去访问成员变量的权限,也就是说在没找到getter
方法,也没找到数组方法的时候程序就会异常:valueForUndefinedKey:
。下面讨论在accessInstanceVariablesDirectly:
返回YES
的情况,开始去找成员变量,具体用法如下:
// Teacher.h
@interface Teacher : NSObject
{
NSString * _name;
NSString * _isName;
NSString * name;
NSString * isName;
}
@end
// Teacher.m
@implementation Teacher
- (instancetype)init {
if (self = [super init]) {
_name = @"_name";
_isName = @"_isName";
name = @"name";
isName = @"isName";
}
return self;
}
@end
上例中在Teacher类中定义了四个成员变量,然后使用KVC取出成员变量name的值,会打输出什么呢?输出结果是:@"_name"
;如果将成员变量_name
及其初始化值注释,输出又会如何呢?输出结果是:@"_isName"
…以此类推。结论很出人意料,虽然使用KVC获取的是成员变量name的值,但是却可以获取到四种值,它首先会到对象的成员变量中寻找名为“_key”
的成员变量,如果有就取出值;如果没有就去找名为“_isKey”
的成员变量,如果有就取出值;如果没有就去找名为“key”
的成员变量,如果有就取出值;如果没有就去找名为“isKey”
的成员变量,如果有就取出值;如果没有程序会crash,报错原因是:
valueForUndefinedKey:
reason: '[<Teacher 0x60000118fd50> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'
那么如果我们不想让程序crash,可以重写valueForUndefinedKey:
方法:
- (id)valueForUndefinedKey:(NSString *)key {
return nil;
}
此时即使key
不存在,会返回null
,而不会异常。
设值 setValue: forKey:
前面说使用valueForKey:
方法获取值,现在说使用setValue: forKey:
设值,那么该方法底层会怎么执行呢?首先回去找setter方法:
- (void)setName:(NSString *)name {
NSLog(@"set name");
}
如果找到没找到setName:
,就会去找:
- (void)setIsName:(NSString *)name {
NSLog(@"set isName");
}
如果没找setIsName:
方法,这里不会再去找别的setter
方法,而是去查找成员变量,在查找之前还是需要看看accessInstanceVariablesDirectly:
的返回值,如果返回NO,则失去访问成员变量的权限,程序creash;如果返回YES,就去查找成员变量,访问成员变量的优先级和valueForKey:
方法一样(_key > _isKey > key > isKey
),如果在查找成员变量时也没有找到key,此时程序crash。
为了避免crash,我们重写setValue: forUndefinedKey:
方法:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"没有找到这个%@",key);
}
注意:如果成员变量是基础数据类型(int、float等),在使用setValue: forKey:
设值时,不能设值为nil,不然程序crash,异常原因setNilValueForKey
:
reason: '[<Teacher 0x6000021b72a0> setNilValueForKey]: could not set nil as the value for the key age.'
这时我们可以重写setNilValueForKey
方法,可以避免这种异常:
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"不能将 %@ 设置为nil",key);
}
我们会注意到valueForKey:
方法返回值是id
类型的,如果我们成员变量是基础数据类型而不是对象类型,这该如何处理呢?对于KVC,Cocoa会自动装箱和开箱标量值,在取值的时候其实返回的是NSNumber
类型。
监听数组 mutableArrayValueForKey:
当数组受到KVO监听时,如果向数组里面添加元素会被监听到吗?例如:
[_teacher addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"new value %@",change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 监听数组的变化
[_teacher.array addObject:@"1"];
}
- (void)dealloc {
[_teacher removeObserver:self forKeyPath:@"array"];
}
当向数组里面添加元素时,显然没有被监听到,这时候需要用到KVC的mutableArrayValueForKey:
方法,该方法返回一个可变数组,然后再向里面添加元素:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 监听数组的变化
//[_teacher.array addObject:@"1"];
static int i = 0;
[[_teacher mutableArrayValueForKey:@"array"] addObject:@(i++)];
NSLog(@"%@",[_teacher mutableArrayValueForKey:@"array"]);
}
KVC的快速运算
先看一个例子:
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
Teacher *t = [[Teacher alloc] init];
t.age = i;
[array addObject:t];
}
利用KVC打印数组的个数:
NSLog(@"%@",[array valueForKey:@"@count"]);
使用@count
运算符来求出数组个数,还有一些常见操作符
@sum
:求和运算符;@min
:求最小值运算符;@max
:求最大值运算符;@avg
:求平均数运算符。
NSLog(@"%@",[array valueForKeyPath:@"@sum.age"]);//求和
NSLog(@"%@",[array valueForKeyPath:@"@min.age"]);//最小值
NSLog(@"%@",[array valueForKeyPath:@"@max.age"]);//最大值
NSLog(@"%@",[array valueForKeyPath:@"@avg.age"]);//平均值
//获取到所有age的一个集合(非数组);特点:无序、不重复
NSLog(@"%@",[array valueForKeyPath:@"@distinctUnionOfObjects.age"]);
注意:这里使用的是valueForKeyPath:
方法,表示键路径,可以在对象和不同变量之间用圆点隔开,这些键路径的深度是任意的,具体取决于对象的复杂度,常用于对象的符合和快速运算,例如一个Person类中复合一个Teacher类,在Teacher类中再复合一个Student类,这时利用KVC取Student类中的一个成员变量的值,可以表示为:
NSString *name = [person valueForKeyPath:@"teacher.student.name"];
同样的还有setValue: forKeyPath:
修改成员变量值的方法:
[person setValue:@"John" forKeyPath:@"teacher.student.name"];
简单实现 setValue: forKey:
下面自己简单的实现以下setValue: forKey:
和valueForKey:
方法,创建一个NSObject的分类:
-(void)YZ_setValue:(id)value forKey:(NSString *)key
{
if (key == nil || key.length == 0) {
return;
}
// 寻找setKey方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(setKey)]) {
[self performSelector:NSSelectorFromString(setKey) withObject:value];
return;
}
// 寻找setIsKey方法
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(setIsKey)]) {
[self performSelector:NSSelectorFromString(setIsKey) withObject:value];
return;
}
// setter方法都没有找到去找成员变量
// 先判断访问成员变量权限
if ([[self class] accessInstanceVariablesDirectly]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
// _key
for (NSInteger i = 0; i< outCount; i++) {
Ivar ivar = ivars[i];
// 成员变量名称
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
// 给成员变量赋值
object_setIvar(self, ivar, value);
return;
}
}
// _isKey
for (NSInteger i = 0; i< outCount; i++) {
Ivar ivar = ivars[i];
// 成员变量名称
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_Is%@",key.capitalizedString]]) {
// 给成员变量赋值
object_setIvar(self, ivar, value);
return;
}
}
// key
for (NSInteger i = 0; i< outCount; i++) {
Ivar ivar = ivars[i];
// 成员变量名称
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:key]) {
// 给成员变量赋值
object_setIvar(self, ivar, value);
return;
}
}
// isKey
for (NSInteger i = 0; i< outCount; i++) {
Ivar ivar = ivars[i];
// 成员变量名称
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"is%@",key.capitalizedString]]) {
// 给成员变量赋值
object_setIvar(self, ivar, value);
return;
}
}
// 释放(new copy create)
free(ivars);
} else {
// 不允许访问成员变量,抛出异常
NSException *exception = [NSException exceptionWithName:@"YZKVC Exception" reason:@"accessInstanceVariablesDirectly method retun NO!" userInfo:nil];
@throw exception;
return;
}
}
- (id)YZ_valueForKey:(NSString *)key {
if (key == nil || key.length == 0) {
return nil;
}
// 先找getter方法
NSString *getKey = [NSString stringWithFormat:@"get%@",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}
if ([self respondsToSelector:NSSelectorFromString(key)]) {
return [self performSelector:NSSelectorFromString(key)];
}
NSString *getIsKey = [NSString stringWithFormat:@"getIs%@",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(getIsKey)]) {
return [self performSelector:NSSelectorFromString(getIsKey)];
}
// 没有查找到getter方法
NSString *countName = [NSString stringWithFormat:@"countOf%@",key.capitalizedString];
NSString *objectInName = [NSString stringWithFormat:@"objectIn%@AtIndex:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(countName)] && [self respondsToSelector:NSSelectorFromString(objectInName)]) {
NSMutableArray *array = [NSMutableArray array];
NSInteger count = [self performSelector:NSSelectorFromString(countName)];
for (NSInteger i = 0; i < count; i++) {
// 这里好像有点问题
id objc = [self performSelector:NSSelectorFromString(objectInName) withObject:@(i)];
NSLog(@"%@ %d",objc,i);
[array addObject:objc];
}
return [NSArray arrayWithArray:array];
}
// 去找成员变量
if ([[self class] accessInstanceVariablesDirectly]) {
// 优先级与YZ_setValue: forKey: 一致
unsigned int outCount = 0;
Ivar * ivars = class_copyIvarList([self class], &outCount);
// _key
for (NSInteger i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
return object_getIvar(self, ivar);
}
}
// _isKey
for (NSInteger i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_is%@",key.capitalizedString]]) {
return object_getIvar(self, ivar);
}
}
// key
for (NSInteger i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:key]) {
return object_getIvar(self, ivar);
}
}
// isKey
for (NSInteger i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"is%@",key.capitalizedString]]) {
return object_getIvar(self, ivar);
}
}
free(ivars);
} else {
NSException *exception = [NSException exceptionWithName:@"YZKVC Exception" reason:@"accessInstanceVariablesDirectly method retun NO!" userInfo:nil];
@throw exception;
return nil;
}
return nil;
}
调用:
Teacher *teacher = [[Teacher alloc] init];
[teacher YZ_setValue:@"John" forKey:@"name"];
NSLog(@"%@",[teacher YZ_valueForKey:@"name"]);