一、kvc基本用法
有如下定义:
typedef struct {
float x, y, z;
} HPPoint;
@interface HotpotCat : NSObject
@property (nonatomic, copy) NSString *name;
@end
@interface HPObject : NSObject {
@public
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) double height;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, assign) HPPoint point;
@property (nonatomic, strong) HotpotCat *hotpotCat;
@end
1.1 一般setter以及成员变量访问
HPObject *obj = [HPObject alloc];
obj.name = @"Hotpot";
obj.age = 18;
obj->nickName = @"Cat";
1.2 kvc间接访问
[obj setValue:@"HotpotCat" forKey:@"name"];
1.3 kvc集合类型
1.3.1 数组
对于如下集合类型数据:
obj.array = @[@"1",@"2",@"3"];
修改方式有两种:
//方式一:获取数组,修改数据。重新设置数组
NSArray *array = [obj valueForKey:@"array"];
array = @[@"100",@"2",@"3"];
[obj setValue:array forKey:@"array"];
NSLog(@"%@",[obj valueForKey:@"array"]);
//方式二:以可变数组接收,然后修改数据
NSMutableArray *mArray = [obj mutableArrayValueForKey:@"array"];
mArray[0] = @"200";
NSLog(@"%@",[obj valueForKey:@"array"]);
- 1.通过
valueForKey:
获取数据后,重新生成新的数组然后setValue: forKey:
设置重新设置值。 - 2.通过
mutableArrayValueForKey:
获取值,直接修改获取到的可变数组。
1.3.2 字典
NSDictionary *dict = @{
@"name":@"Hotpot",
@"age":@18,
@"height":@180.0
};
HPObject *obj = [[HPObject alloc] init];
//字典转模型
[obj setValuesForKeysWithDictionary:dict];
NSLog(@"%@",obj);
//key数组->模型到字典
NSArray *keyArray = @[@"name",@"age",@"height"];
NSDictionary *valueDic = [obj dictionaryWithValuesForKeys:keyArray];
NSLog(@"%@",valueDic);
字典模型互转。
1.3.3 kvc消息传递
NSArray *array = @[@"Hotpot",@"Cat",@"HotpotCat"];
NSArray *lenStr= [array valueForKeyPath:@"length"];
// 消息从array传递给了string
NSLog(@"lenStr:%@",lenStr);
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"lowStr:%@",lowStr);
输出:
lenStr:(
6,
3,
9
)
lowStr:(
hotpot,
cat,
hotpotcat
)
-
length
以及lowercaseString
消息通过array
传递给了string
。
1.3.4 kvc 聚合操作符
NSMutableArray *dataArray = [NSMutableArray array];
for (int i = 0; i < 5; i++) {
HPObject *p = [HPObject new];
NSDictionary* dict = @{
@"name":@"Hotpot",
@"age":@(18+i),
@"height":@(175.0 + 2 * arc4random_uniform(5)),
};
[p setValuesForKeysWithDictionary:dict];
[dataArray addObject:p];
}
//身高数据
NSLog(@"%@", [dataArray valueForKey:@"height"]);
//平均身高
float avg = [[dataArray valueForKeyPath:@"@avg.height"] floatValue];
NSLog(@"height avg:%f", avg);
//总数
int count = [[dataArray valueForKeyPath:@"@count.height"] intValue];
NSLog(@"height count:%d", count);
//身高和
int sum = [[dataArray valueForKeyPath:@"@sum.height"] intValue];
NSLog(@"height sum:%d", sum);
//最大身高
int max = [[dataArray valueForKeyPath:@"@max.height"] intValue];
NSLog(@"height max:%d", max);
//最小身高
int min = [[dataArray valueForKeyPath:@"@min.height"] intValue];
NSLog(@"height min:%d", min);
输出:
(
177,
183,
185,
177,
179
)
height avg:180.199997
height count:5
height sum:901
height max:185
height min:177
可以通过valueForKeyPath
配合操作符@avg、@count、@max、@min、@sum
对数据进行处理。
1.3.5 kvc 数组操作符
对于1.3.4
中的示例代码增加以下逻辑:
NSLog(@"dataArray height:%@", [dataArray valueForKey:@"height"]);
// 返回操作对象指定属性的集合
NSArray *heights = [dataArray valueForKeyPath:@"@unionOfObjects.height"];
NSLog(@"heights:%@", heights);
// 返回操作对象指定属性的集合 -- 去重
NSArray *heightsWithoutDuplicates = [dataArray valueForKeyPath:@"@distinctUnionOfObjects.height"];
NSLog(@"heights:%@", heightsWithoutDuplicates);
输出:
dataArray height:(
181,
183,
177,
177,
181
)
heights:(
181,
183,
177,
177,
181
)
heights:(
181,
177,
183
)
-
@unionOfObjects
返回操作对象的指定属性的集合。 -
@distinctUnionOfObjects
返回操作对象指定属性的集合(会进行去重)。
1.3.5.1 数组集合嵌套
NSMutableArray *dataArray = [NSMutableArray array];
for (int i = 0; i < 5; i++) {
HPObject *p = [HPObject new];
NSDictionary* dict = @{
@"name":@"Hotpot",
@"age":@(18 + i),
@"height":@(175.0 + 2 * arc4random_uniform(5)),
};
[p setValuesForKeysWithDictionary:dict];
[dataArray addObject:p];
}
// 嵌套数组
NSArray *newArr = @[dataArray,dataArray];
NSLog(@"newArr height:%@", [newArr valueForKey:@"height"]);
// 返回指定属性的集合
NSArray *heights = [newArr valueForKeyPath:@"@unionOfArrays.height"];
NSLog(@"heights:%@", heights);
// 返回指定属性的集合 -- 去重
NSArray *heightsWithoutDuplicates = [newArr valueForKeyPath:@"@distinctUnionOfArrays.height"];
NSLog(@"heights:%@", heightsWithoutDuplicates);
⚠️当然这里数组中对象可以不同,只要都有
height
即可。
输出:
newArr height:(
(
183,
181,
177,
177,
179
),
(
183,
181,
177,
177,
179
)
)
heights:(
183,
181,
177,
177,
179,
183,
181,
177,
177,
179
)
heights:(
183,
179,
181,
177
)
-
@unionOfArrays
将数组中嵌套的集合进行解析,获取指定属性的集合。@ distinctUnionOfArrays
会对获取到的属性值进行去重。
1.3.5.2 set集合嵌套
NSMutableSet *dataSet = [NSMutableSet set];
for (int i = 0; i < 5; i++) {
HPObject *p = [HPObject new];
NSDictionary* dict = @{
@"name":@"Hotpot",
@"age":@(18 + i),
@"height":@(175.0 + 2 * arc4random_uniform(5)),
};
[p setValuesForKeysWithDictionary:dict];
[dataSet addObject:p];
}
NSMutableSet *dataSet2 = [NSMutableSet set];
for (int i = 0; i < 5; i++) {
HPObject *p = [HPObject new];
NSDictionary* dict = @{
@"name":@"Hotpot",
@"age":@(18 + i),
@"height":@(175.0 + 2 * arc4random_uniform(5)),
};
[p setValuesForKeysWithDictionary:dict];
[dataSet2 addObject:p];
}
// 嵌套set
NSSet *newSet = [NSSet setWithObjects:dataSet, dataSet2, nil];
NSLog(@"newSet height:%@", [newSet valueForKey:@"height"]);
// 返回指定属性的集合
NSArray *heightsWithoutDuplicates = [newSet valueForKeyPath:@"@distinctUnionOfSets.height"];
NSLog(@"heights:%@", heightsWithoutDuplicates);
输出:
newSet height:{(
{(
175,
181,
183
)},
{(
175,
177,
183,
179
)}
)}
heights:{(
175,
181,
177,
183,
179
)}
-
@distinctUnionOfSets
可以获取嵌套的set
中对象的属性集合。
1.4 kvc访问非对象属性
HPPoint point = {1.,2.,3.};
//结构体转换为NSValue
NSValue *value = [NSValue valueWithBytes:&point objCType:@encode(HPPoint)];
HPObject *obj = [HPObject alloc];
[obj setValue:value forKey:@"point"];
NSValue *pointValue = [obj valueForKey:@"point"];
NSLog(@"pointValue:%@",pointValue);
NSLog(@"obj--- x:%f y:%f z:%f",obj.point.x,obj.point.y,obj.point.z);
HPPoint point1;
//NSValue 转换为 结构体
[pointValue getValue:&point1];
NSLog(@"NSValue--- x:%f y:%f z:%f",point1.x,point1.y,point1.z);
输出:
pointValue:{length = 12, bytes = 0x0000803f0000004000004040}
obj--- x:1.000000 y:2.000000 z:3.000000
NSValue--- x:1.000000 y:2.000000 z:3.000000
- 通过
NSValue
与结构体互转,存取值。
1.5 kvc 层层访问(keyPath)
HPObject *obj = [HPObject alloc];
obj.name = @"obj";
HotpotCat *hp = [HotpotCat alloc];
hp.name = @"hp";
obj.hotpotCat = hp;
[obj setValue:@"cat" forKeyPath:@"hotpotCat.name"];
NSLog(@"%@",[obj valueForKeyPath:@"hotpotCat.name"]);
二、kvc原理分析
- (void)setValue:(nullable id)value forKey:(NSString *)key;
定义在NSObject(NSKeyValueCoding)
分类中(Foundation
框架),没有开源。
在苹果的官方文档中有对kvc
的详细介绍:
Key-Value Coding Programming Guide
2.1 kvc设值原理
@interface HPObject : NSObject{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@end
Accessor Search Patterns
setValue:forKey:
根据官方文档的介绍普通对象逻辑如下:
1. set
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
调用:
HPObject *obj = [HPObject alloc];
[obj setValue:@"HP" forKey:@"name"];
- 两个都实现先调用
setName
,setName
没用实现则调用_setName
。
2. 实例变量设置值 _
先通过accessInstanceVariablesDirectly
控制是否开启实例变量访问,默认开启。
既然会通过这4
个成员变量进行赋值,那么在都存在的情况下验证下优先级:
HPObject *obj = [HPObject alloc];
[obj setValue:@"Hotpot" forKey:@"name"];
NSLog(@"_name:%@,_isName:%@,name:%@,isName:%@",obj->_name,obj->_isName,obj->name,obj->isName);
输出:
_name:Hotpot,_isName:(null),name:(null),isName:(null)
可以看到实际上只对一个成员变量进行了赋值。那么肯定就有优先级,逐个屏蔽成员变量有以下输出:
_name:Hotpot,_isName:(null),name:(null),isName:(null)
_isName:Hotpot,name:(null),isName:(null)
name:Hotpot,isName:(null)
isName:Hotpot
优先级是_
。
⚠️这个时候有个疑问,既然第一步中有set
,那是不是也应该有setIs
?
实现setIsName
与_setIsName
:
- (void)setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
//没有调用
- (void)_setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
与1
中的验证逻辑相同,setIsName
会优先调用,但是_setIsName
没有调用。
所以1
中的调用优先级是set
。
3. 以上操作都失败,进入setValue:forUndefinedKey:
如果上面的方法没有实现,实例变量也都没有,则会走到setValue:forUndefinedKey:
逻辑,相当于是对于异常可以在这里处理。实现后找不到就会走到这个流程,不会crash
了。
2.2 kvc取值原理
1. get
实现以下代码:
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
验证:
HPObject *obj = [HPObject alloc];
NSLog(@"getter value:%@",[obj valueForKey:@"name"]);
逐个屏蔽实现的4
个方法有如下调用:
getter value:getName
getter value:name
getter value:isName
getter value:_name
- 调用顺序为:
get
-> -> is -> _
2. 普通类型方法没有找到,去找countOf
、objectIn
首先要实现countOf
,然后实现objectIn
中的一个。此时意味着当前对象拥有一个属性名为
的NSKeyValueArray
类型的属性,它可以响应NSArray
的所有方法。一个对象不一定需要显式的写出自己的属性也可以进行存取操作。
//countOf
- (NSInteger)countOfName {
NSLog(@"%s",__func__);
return 1;
}
//objectInAtIndex:
- (id)objectInNameAtIndex:(NSInteger)index {
return @"name1";
}
//AtIndexes:
- (NSArray *)nameAtIndexes:(NSIndexSet *)indexes {
return @[@"name1"];
}
调用:
HPObject *obj = [HPObject alloc];
NSLog(@"getter value:%@",[obj valueForKey:@"name"]);
输出:
getter value:(
name1
)
- 这个时候获取到的值是一个集合,数量由
countOf
控制。 - 优先级:
objectIn
AtIndex: - > AtIndexes:
3. 没有找到NSArray简单存取方法,或者NSArray存取方法组。则查找countOfNSSet
)
//countOf
- (NSInteger)countOfName {
NSLog(@"%s",__func__);
return 5;
}
//enumeratorOf
- (NSEnumerator *)enumeratorOfName {
NSLog(@"%s",__func__);
NSSet *set = [NSSet setWithArray:@[@"name1",@"name2",@"name3"]];
NSEnumerator *enumerator = [set objectEnumerator];
return enumerator;
}
//memberOf:
- (id)memberOfName:(id)obj {
NSLog(@"%s",__func__);
return @"name1";
}
如果找到这三个方法,则创建一个集合代理对象,该对象响应所有NSSet
方法并返回。否则,继续执行第4
步。
⚠️这块代码调试有问题。
4. 实例变量取值 _
accessInstanceVariablesDirectly
返回YES
HPObject *obj = [HPObject alloc];
obj->_name = @"_name";
obj->_isName = @"_isName";
obj->name = @"name";
obj->isName = @"isName";
NSLog(@"getter value:%@",[obj valueForKey:@"name"]);
逐个屏蔽有如下输出:
getter value:_name
getter value:_isName
getter value:name
getter value:isName
调用顺序优先级:_
5. 数据类型返回处理
如果取回的是一个对象指针,则直接返回这个结果。如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber
支持的,则存储为NSNumber
并返回。如果取回的是一个不支持NSNumber
的基础数据类型,则通过NSValue
进行存储并返回。
6. 以上都没有实现进入 valueForUndefinedKey:
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"%s --- key:%@",__func__,key);
return nil;
}
实现valueForUndefinedKey
后就不会崩溃了。可以在这里进行异常收集和处理。
总结
-
设值:
- 1.
set
相关方法调用设置,优先级set
。-> _set -> setIs - 2.先判断是否允许访问实例变量,允许的情况下给对应的实例变量赋值,优先级
_
-> _is -> -> is - 3.在没有相关
set
方法以及实例变量(不允许访问也包括)会判断setValue:forUndefinedKey:
是否实现进行调用,没有实现的情况下直接crash
报错,实现了就交给它处理。
- 1.
-
取值:
- 1.
get
相关方法调用获取值,优先级get
。-> -> is -> _ - 2.普通类型方法没有找到,排查
countOf
是否实现,实现的情况下判断objectIn
是否有一个实现。这个时候就相当于是对数组的操作了。优先级AtIndex:/ AtIndexes: objectIn
。AtIndex -> AtIndexes: - 3.没有找到
NSArray
简单存取方法,或者NSArray
存取方法组。则查找countOf
命名的方法。相当于是一个、enumeratorOf 、memberOf NSSet
。 - 4.如果允许访问实例变量,则从实例变量取值,优先级
_
。-> _is -> -> is - 5.对返回的值进行判断,是指针直接返回;负责基础类型能被
NSNumber
支持就存储为NSNumber
返回,不能则用NSValue
存储返回。 - 6.在以上操作都失败的情况下判断
valueForUndefinedKey:
有没有实现,实现的情况下交给valueForUndefinedKey:
处理异常,否则报错crash
。
- 1.
kvc取值设置流程:
三、自定义kvc
上面分析了kvc
的实现原理,那么如果按照这个规则自己可以实现一套(仅针对基本类型)。
给NSObject
添加一个分类HP_KVC
。
3.1自定义kvc设值实现
按照上面的分析实现要分为4
步:
1.参数容错处理
if (key == nil || key.length == 0) return;
当key
值不存在的时候直接返回。
2.set相关方法判断调用 set
// set -> _set -> setIs
- (BOOL)hp_handleSetMethodsWithKey:(NSString *)key value:(id)value {
//首字母大写
NSString *Key = key.capitalizedString;
//拼接方法名
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
//方法调用
if ([self hp_performSelectorWithMethodName:setKey value:value]) {
return YES;
} else if ([self hp_performSelectorWithMethodName:_setKey value:value]) {
return YES;
} else if ([self hp_performSelectorWithMethodName:setIsKey value:value]) {
return YES;
}
return NO;
}
- (BOOL)hp_performSelectorWithMethodName:(NSString *)methodName value:(id)value {
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
return YES;
}
return NO;
}
- 根据
key
值拼接方法进行调用,有实现返回YES
,没有实现返回NO
。
3.实例变量调用
// 2.实例变量调用
- (BOOL)hp_handleSetIvarsWithKey:(NSString *)key value:(id)value {
// 2.1 accessInstanceVariablesDirectly
if (![self.class hp_accessInstanceVariablesDirectly]) {//不能调用实例变量
//@TODO 这里需要补充最后的异常处理逻辑
return YES;
}
// 2.2 _ -> _is -> -> is
//获取实例变量名
NSMutableArray *ivarNamesArray = [self hp_getIvarListName];
//首字母大写
NSString *Key = key.capitalizedString;
//拼接方法名
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
//判断ivarNamesArray中是否存在实例变量
if ([ivarNamesArray containsObject:_key]) {
// 获取ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 实例变量设置值
object_setIvar(self , ivar, value);
return YES;
} else if ([ivarNamesArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return YES;
} else if ([ivarNamesArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return YES;
} else if ([ivarNamesArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return YES;
}
return NO;
}
+ (BOOL)hp_accessInstanceVariablesDirectly {
return YES;
}
- (NSMutableArray *)hp_getIvarListName {
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *ivarNameChar = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName: %@",ivarName);
[mArray addObject:ivarName];
}
free(ivars);
return mArray;
}
- 先判断
hp_accessInstanceVariablesDirectly
是否允许访问实例变量。 - 不允许则进行异常处理逻辑。
- 允许则根据
key
拼接_
,获取类的所有实例变量名进行比较匹配。匹配到就设值,没有匹配到就进行下一步。-> _is -> -> is
4.异常处理
- (void)hp_setValue:(id)value forUndefinedKey:(NSString *)key;
//异常处理
- (void)hp_handleSetExceptionWithKey:(NSString *)key value:(id)value {
if (![self hp_performSelectorWithMethodName:@"hp_setValue:forUndefinedKey:" value:key]) {
@throw [NSException exceptionWithName:@"HP_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key : %@,value:%@",self,NSStringFromSelector(_cmd),key,value] userInfo:nil];
}
}
- 实现了
hp_setValue : forUndefinedKey :
就调用,否则抛出异常。
hp_setValue:forKey:实现如下:
- (void)hp_setValue:(nullable id)value forKey:(NSString *)key {
// 0.参数容错处理
if (key == nil || key.length == 0) return;
// 1.set相关方法调用 set -> _set -> setIs
if ([self hp_handleSetMethodsWithKey:key value:value]) {
return;
}
// 2.实例变量调用
// 2.1 accessInstanceVariablesDirectly
// 2.2 _ -> _is -> -> is
if ([self hp_handleSetIvarsWithKey:key value:value]) {
return;
}
// 3.异常处理 setValue:forUndefinedKey:
[self hp_handleSetExceptionWithKey:key value:value];
}
3.2 自定义kvc取值实现
1.参数容错处理
if (key == nil || key.length == 0) return nil;
当key
值不存在的时候直接返回nil
。
2.get相关方法处理
//key 首字母大写
NSString *Key = key.capitalizedString;
//拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
//方法调用
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
} else if ([self respondsToSelector:NSSelectorFromString(key)]) {
return [self performSelector:NSSelectorFromString(key)];
} else if ([self respondsToSelector:NSSelectorFromString(isKey)]) {
return [self performSelector:NSSelectorFromString(isKey)];
} else if ([self respondsToSelector:NSSelectorFromString(_key)]) {
return [self performSelector:NSSelectorFromString(_key)];
}
#pragma clang diagnostic pop
- 拼接方法判断调用然后返回方法的返回值。
3.实例变量取值
//实例变量取值
// 2.1 accessInstanceVariablesDirectly
if (![self.class hp_accessInstanceVariablesDirectly]) {//不能调用实例变量
//@TODO异常处理
}
// 2.2 实例变量进行取值
NSMutableArray *ivarNamesArray = [self hp_getIvarListName];
// _ -> _is -> -> is
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
if ([ivarNamesArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
} else if ([ivarNamesArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
} else if ([ivarNamesArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
} else if ([ivarNamesArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
- 首先根据
hp_accessInstanceVariablesDirectly
判断能否从实例变量取值。 - 拼接实例变量名称,获取所有实例变量名。
- 获取实例变量的值。
4.异常处理
- (id)hp_handleGetExceptionWithKey:(NSString *)key {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(@"hp_valueForUndefinedKey:")]) {
return [self performSelector:NSSelectorFromString(@"hp_valueForUndefinedKey:") withObject:key];
}
#pragma clang diagnostic pop
@throw [NSException exceptionWithName:@"HP_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ hp_valueForUndefinedKey]: this class is not key value coding-compliant for the key : %@",self,key] userInfo:nil];
}
- 判断
hp_valueForUndefinedKey:
是否实现,实现了则调用。 - 没有实现报错。
3.3 自定义kvc完整实现
@interface NSObject (HP_KVC)
@property (class, readonly) BOOL hp_accessInstanceVariablesDirectly;
- (void)hp_setValue:(nullable id)value forKey:(NSString *)key;
- (void)hp_setValue:(id)value forUndefinedKey:(NSString *)key;
- (id)hp_valueForKey:(NSString *)key;
- (id)hp_valueForUndefinedKey:(NSString *)key;
@end
#import "NSObject+HP_KVC.h"
#import
@implementation NSObject (HP_KVC)
- (void)hp_setValue:(nullable id)value forKey:(NSString *)key {
// 0.参数容错处理
if (key == nil || key.length == 0) return;
// 1.set相关方法调用 set -> _set -> setIs
if ([self hp_handleSetMethodsWithKey:key value:value]) {
return;
}
// 2.实例变量调用
// 2.1 accessInstanceVariablesDirectly
// 2.2 _ -> _is -> -> is
if ([self hp_handleSetIvarsWithKey:key value:value]) {
return;
}
// 3.异常处理 setValue:forUndefinedKey:
[self hp_handleSetExceptionWithKey:key value:value];
}
// set -> _set -> setIs
- (BOOL)hp_handleSetMethodsWithKey:(NSString *)key value:(id)value {
//首字母大写
NSString *Key = key.capitalizedString;
//拼接方法名
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
//方法调用
if ([self hp_performSelectorWithMethodName:setKey value:value]) {
return YES;
} else if ([self hp_performSelectorWithMethodName:_setKey value:value]) {
return YES;
} else if ([self hp_performSelectorWithMethodName:setIsKey value:value]) {
return YES;
}
return NO;
}
- (BOOL)hp_performSelectorWithMethodName:(NSString *)methodName value:(id)value {
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
return YES;
}
return NO;
}
// 2.实例变量调用
- (BOOL)hp_handleSetIvarsWithKey:(NSString *)key value:(id)value {
// 2.1 accessInstanceVariablesDirectly
if (![self.class hp_accessInstanceVariablesDirectly]) {//不能调用实例变量
//@TODO 这里需要补充最后的异常处理逻辑
[self hp_handleSetExceptionWithKey:key value:value];
return YES;
}
// 2.2 _ -> _is -> -> is
//获取实例变量名
NSMutableArray *ivarNamesArray = [self hp_getIvarListName];
//首字母大写
NSString *Key = key.capitalizedString;
//拼接方法名
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
//判断ivarNamesArray中是否存在实例变量
if ([ivarNamesArray containsObject:_key]) {
// 获取ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 实例变量设置值
object_setIvar(self , ivar, value);
return YES;
} else if ([ivarNamesArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return YES;
} else if ([ivarNamesArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return YES;
} else if ([ivarNamesArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return YES;
}
return NO;
}
+ (BOOL)hp_accessInstanceVariablesDirectly {
return YES;
}
- (NSMutableArray *)hp_getIvarListName {
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *ivarNameChar = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName: %@",ivarName);
[mArray addObject:ivarName];
}
free(ivars);
return mArray;
}
//3 异常处理
- (void)hp_handleSetExceptionWithKey:(NSString *)key value:(id)value {
if (![self hp_performSelectorWithMethodName:@"hp_setValue:forUndefinedKey:" value:key]) {
@throw [NSException exceptionWithName:@"HP_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key : %@,value:%@",self,NSStringFromSelector(_cmd),key,value] userInfo:nil];
}
}
- (id)hp_valueForKey:(NSString *)key {
//0 key容错处理
if (key == nil || key.length == 0) return nil;
//1 get相关方法处理
//key 首字母大写
NSString *Key = key.capitalizedString;
//拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
//方法调用
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
} else if ([self respondsToSelector:NSSelectorFromString(key)]) {
return [self performSelector:NSSelectorFromString(key)];
} else if ([self respondsToSelector:NSSelectorFromString(isKey)]) {
return [self performSelector:NSSelectorFromString(isKey)];
} else if ([self respondsToSelector:NSSelectorFromString(_key)]) {
return [self performSelector:NSSelectorFromString(_key)];
}
#pragma clang diagnostic pop
//实例变量取值
// 2.1 accessInstanceVariablesDirectly
if (![self.class hp_accessInstanceVariablesDirectly]) {//不能调用实例变量
return [self hp_handleGetExceptionWithKey:key];
}
// 2.2 实例变量进行取值
NSMutableArray *ivarNamesArray = [self hp_getIvarListName];
// _ -> _is -> -> is
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
if ([ivarNamesArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
} else if ([ivarNamesArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
} else if ([ivarNamesArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
} else if ([ivarNamesArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
// 3 异常处理
return [self hp_handleGetExceptionWithKey:key];
}
- (id)hp_handleGetExceptionWithKey:(NSString *)key {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(@"hp_valueForUndefinedKey:")]) {
return [self performSelector:NSSelectorFromString(@"hp_valueForUndefinedKey:") withObject:key];
}
#pragma clang diagnostic pop
@throw [NSException exceptionWithName:@"HP_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ hp_valueForUndefinedKey]: this class is not key value coding-compliant for the key : %@",self,key] userInfo:nil];
}
@end
当然这里只是实现了基本类型以及单层path
的逻辑。github
上有很多自定的实现,原理都差不多。具体可以参考别人比较优秀的实现:DIS_KVC_KVO
四、kvc小技巧
typedef struct {
float x, y, z;
} HPPoint;
@interface HotpotCat : NSObject
@property (nonatomic, copy) NSString *name;
@end
@interface HPObject : NSObject {
@public
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) double height;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, assign) HPPoint point;
@property (nonatomic, strong) HotpotCat *hotpotCat;
@property (nonatomic, assign) bool married;
@end
4.1 kvc 自动转换类型
HPObject *obj = [HPObject alloc];
//age是int类型,需要转成NSNumber
[obj setValue:@18 forKey:@"age"];
NSLog(@"int -> number value:%@ type:%@",[obj valueForKey:@"age"],[[obj valueForKey:@"age"] class]);
//由于age是int类型,存的是字符串@"20",kvc自动转换成NSCFNumber
[obj setValue:@"20" forKey:@"age"];
NSLog(@"string -> number value:%@ type:%@",[obj valueForKey:@"age"],[[obj valueForKey:@"age"] class]);
//由于sex是Bool类型,存的是字符串@“20”,KVC自动转换成NSCFNumber
[obj setValue:@"20" forKey:@"married"];
NSLog(@"bool -> number value:%@ type:%@",[obj valueForKey:@"sex"],[[obj valueForKey:@"sex"] class]);
输出:
int -> number value:18 type:__NSCFNumber
string -> number value:20 type:__NSCFNumber
BOOL -> number value:1 type:__NSCFBoolean
由于age
是int
类型需要转换为NSNumber
可以理解。在用NSString
给age
赋值时,自动转换为了NSCFNumber
。在用NSString
给bool
类型赋值时自动转换为了NSCFBoolean
类型。
4.2 kvc 设置空值
[obj setValue:nil forKey:@"age"];
[obj setValue:nil forKey:@"name"];
当给age
设置nil
时直接报错:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[ setNilValueForKey]: could not set nil as the value for the key age.'
给name
设置成功,没有任何输出。在HPObject
中实现setNilValueForKey
:
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"%s key:%@",__func__,key);
}
这个时候age
设置空值会进入到setNilValueForKey
,name
不会。
根据官方的注释setNilValueForKey
只能处理NSNumber
和NSValue
结构体。
4.3 找不到key
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"%s key:%@, value:%@",__func__,key,value);
}
- (id)valueForUndefinedKey:(NSString *)key{
NSLog(@"%s key:%@",__func__,key);
return nil;
}
取值和设置找不到key
的时候分别对应valueForUndefinedKey:
与setValue:forUndefinedKey:
。可以在这里进行容错处理。
4.4 键值验证
验证能不能设置某个值:
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
先找一下类中是否实现了方法-(BOOL)validate
,实现了就会根据实现方法里面的自定义逻辑返回NO
/YES
,没有实现这个方法,系统默认返回YES
。
比如对于name
而言:
- (BOOL)validateName:(id *)value error:(out NSError *__autoreleasing _Nullable *)outError {
NSLog(@"%s",__func__);
NSString *name = *value;
if ([name isEqualToString:@"HP"]) {
return YES;
}
return NO;
}
//调用
NSError *error;
NSString *name = @"HP1";
if (![obj validateValue:&name forKey:@"name" error:&error]) {
NSLog(@"%@",error);
} else {
NSLog(@"%@",[obj valueForKey:@"name"]);
}
只有name
为HP
的时候才验证通过。
重写同时验证key和value:
当然也可以重写validateValue: validateValue : error:
,重写后就不走-(BOOL)validate
的逻辑了:
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
if([inKey isEqualToString:@"name"]){
[self setValue:[NSString stringWithFormat:@"HP_%@",*ioValue] forKey:inKey];
return YES;
}
*outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"key not allowed set:%@ - %@ ",inKey,self] code:10086 userInfo:nil];
return NO;
}
可以在这里进行键值验证、容错 、 派发 、 消息转发
处理。