iOS KVC分析
序言
2022年4月初,面试的时候被KVC的相关问题尬了。当时被问到KVC的时候有种懵逼的感觉,可以说大概都知道,但是面试没准备,对这KVC的东西都有种朦胧的感觉,全都是 set<字符串逆序输出;key>
,_s字符间距加宽2磅et<key>
,_<key>
,is<k变量之间的关系ey>
这些,没有一个具体的概念。 现在有时间就查了一些资料,然后在这里做个记面试自我介绍3分钟通用录。
一、体验KVC
体验直接开始代码感受一下,如下所示:
1、基本类型
LGPerson.h:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END
LGPerson.m:
#import "LGPerson.h"
@implementation LGPerson
@end
KVC的体验:
LGPerson *p = [LGPerson new];
p.name = @"zlg";
[p setValue:@"leilei" forKey:@"name"];
NSLog(@"%@",p.name);
输出结果:
输出结果是leilei
而不是zlg
。
2、集测试手机是否被监控合类型
LGPerson.h加入数组:
@property (nonatomic, strong) NSArray *array;
体验:
p.array = @[@"0",@"1",@"2",@"3",@"4"];
// 1、直接新数组赋值
NSArray *array = @[@"2",@"3",@"4"];
[p setValue:array forKey:@"array"];
NSLog(@"直接新数组赋值: %@", p.array);
// 2、拷贝赋值
NSMutableArray *mArray = [p mutableArrayValueForKey:@"array"];
mArray[0] = @"0";
NSLog(@"拷贝赋值: %@", p.array);
输出结接口测试果:
简单的体验一下KVC。
二、认识KVC
KVC是NSKeyValueCoding非正式协议支持的一种机制,对象采用这种机制来提供对其属性的间接访问。当一个对象符合键值编码时,它的属性可以通过一个简洁、统变量英语一的消息传递接口通面试自我介绍一分钟过字符串参数寻址。这种间接访问机制补充了实例变量及其关联访问器方法所提供的直接访问。
这段引用自官方文档
那么KVC是面试问题赋值和取值的机制是怎么样的呢?在官方文档里有这么接口是什么一个解释简单翻译一下:
- 赋值
- 查找方法:按照
set<Key>:
和_set<Key>
的顺序查找 - 查找实例变量:判断
accessInstanceVariable变量与函数sDirectly
返回值为YES
,然后按照_<key>
,_is<Key>
,<key>
, 和is<Key>
的顺序查找
- 取值
- 查找方法:按照
get<Key>
,<key>
,is<Key>
, 和_<key>
的顺序查找 - 查找集合方法(不探究):
countOf<Key>
,ob接口和抽象类的区别jectIn<Key>AtIndex面试技巧和注意事项:
,<key>AtIndexes:
,coun接口类型tOf<Key>
,objectIn<Key>AtIndex:
,<key>AtIndexes:
,countOf<Key>
,enumeratorOf<Key>
,membe测试仪rOf<Key>:
等 - 查找实例变量:测试你适合学心理学吗判断
accessInstanceVariablesDirectly
返回值为YES
,然后按照_<key>
,_is<Key>
,<key>
, 和is<Key&接口和抽象类的区别gt;
的顺序查找
accessInstanceVariablesDirectly
这个测试用例方法在KVC的取值和赋值中都有用到,我们看一下官方给它的解释:
如果KVC方法的默认实现在没有找到属性的访问方法时应该直接访问对应的实例变量,则返回YES。如果不应该返回NO。NSObject对这个方法的实现返回YES。子类可以重写它以返回NO,在这种情况下,其他方法将不会访问实例变量。
所以它默认的返回值是YES
。
2.1、接口赋值验证
2.1.1、方法查找验证
将LGPerson.h
中的字符是什么name
属性注测试英文销
LGPerson.m
中实现setName:
和_setName:测试英文
方测试英文法,如下:
#import "LGPerson.h"
@implementation LGPerson
- (void)setName:(NSString *)name{
NSLog(@"%s--%@", __func__, name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s--%@", __func__, name);
}
@end
然后调用:
LGPerson *p = [LGPerson new];
[p setValue:@"leilei" forKey:@"name"];
运行结果:
setName:
方法,注释此方法再次运行,结果如下:
_setName:
方法,
验证了赋值的方法查找。
2.1.2、实例变量查找验证
将方法注释了,添加4个实例变量_name
,_i变量英语sName
,name
和isName
并实现
LGPerson.h
如下:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject {
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
//@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *array;
@end
NS_ASSUME_NONNULL_END
LGPerson.m
如下:
#import "LGPerson.h"
@implementation LGPerson
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
//- (void)setName:(NSString *)name{
// NSLog(@"%s--%@", __func__, name);
//}
//- (void)_setName:(NSString *)name{
// NSLog(@"%s--%@", __func__, name);
//}
@end
调用:
LGPerson *p = [LGPerson new];
[p setValue:@"leilei" forKey:@"name"];
NSLog(@"_name:%@",p->_name);
NSLog(@"_isName:%@",p->_isName);
NSLog(@"name:%@",p->name);
NSLog(@"isName:%@",p->isName);
运行结果:
ac字符串是什么意思cessInstanceVariablesDirectly
的返回值设为NO
不支持实例变量的查找,将将accessInstanceVariablesDirectly
的返回值设为YES
再次运行:
_isName变量名的命名规则
有值。注释_isNam面试问题e
后运行结果:
name
有值。注释name
后运面试问题大全及答案大全行结果:
isName
的情况下才会给isName
赋值。
至此赋值即setValue:forKey:
的验证结束测试英文,得出结论:接口英文
KVC在使用
setVa面试问题大全及答案大全lue:forKey:
赋值时,先查找set<key>
然后查找_set<key>
这2个方法,如果未找到则会判断accessInstanc变量名eVariablesDirectly
返回值,为NO
直接抛出异常,为YES
继续查找实例变量,查找顺序为_name
,_isName
,n面试问题ame
和isName
。
2.2、取值验证
2.2.1、方法查找验证
首先把LGPerson.h
中的方法全部注释,然后在LGPerson.m
添加getName
,name
,字符是什么isName
,_name
,accessInstanceVariablesDirectly
返回值为NO
,如下所示:
#import "LGPerson.h"
@implementation LGPerson
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
//- (void)setName:(NSString *)name{
// NSLog(@"%s--%@", __func__, name);
//}
//- (void)_setName:(NSString *)name{
// NSLog(@"%s--%@", __func__, name);
//}
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
@end
调用如下:
LGPerson *p = [LGPerson new];
NSLog(@"取到的值%@",[p valueForKey:@"name"]);
运行结果如图:
2.2接口.2、实例变量查找验证
添加4个实例变量_name
,_isName
,name
和isName
并实现ac面试自我介绍3分钟通用cessInstanceVariablesDirectly
返回值为直接设为NO
,跟赋值时候类似(见2.1.2)
调用如下:变量英语
LGPerson *p = [LGPerson new];
p->name = @"name";
p->_name = @"_name";
p->_isName = @"_isName";
p->isName = @"isName";
NSLog(@"取到的值%@",[p valueForKey:@"name"]);
运行结果如下:
accessInstan面试自我介绍简单大方ceVariablesDirectly
返回值为直接设为YES
KVO取值变量值即
valueForKey:
先查找方法,按照getName
,name
,isName
和接口_name
的面试顺序字符间距怎么加宽进行查找。没接口crc错误计数有找到对应方法则会对accessInstanceVariablesDirectly
进行判断,若为NO
直接抛出异常,为YES
则继续进行实例变量查找,查找顺序为_name
,_isName
,name
和isName
。
2.3、KVC变量名的命名规则防护
实际上在前面字符间距在哪里设置的验证中除了已经列出来的2个异常,在所有的查找测试工程师方式都没结果之后都会直接抛出异常。如何避免呢?在官方的说明中有2个方法valueForUndefinedKey:
和setValue:forUndefinedK面试自我介绍一分钟ey:
用来处理取值和赋值的时候找不到key
而引发的异常,我们来简单的验证一下,在LGPerson测试抑郁程度的问卷.m中实现这2个变量名的命名规则方法,如下所示:
- (id)valueForUndefinedKey:(NSString *)key{
NSLog(@"错误的key:%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"错误的key:%@, 值:%@", key, value);
}
调用
LGPerson *p = [LGPerson new];
[p setValue:@"123" forKey:@"age"];
NSLog(@"取到的值%@",[p valueForKey:@"age"]);
并没有age
相关的方法和实例变量,运行结果如下:
valueForUndefinedKey:
和setValue:forUndefinedKey:
方法。这样我们就避免了KVC引起的异常。
3、自定义KVC变量泵
KVC的赋测试工程师值与取值流程基本上已经探索完毕,过程也没有那么困难,所以我们来自定义一个我们自己的KVC。
首先创建一个LGKVC
的分类,如下:
NSObject+LGKVC.h
:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (LGKVC)
- (void)LG_setValue:(id)value forKey:(NSString *)key;
- (id)LG_valueForKey:(NSString *)key;
@end
NS_ASSUME_NONNULL_END
NSObject+LGKVC.m
:
#import "NSObject+LGKVC.h"
@implementation NSObject (LGKVC)
- (void)LG_setValue:(id)value forKey:(NSString *)key{
}
- (id)LG_valueForKey:(NSString *)key{
return nil;
}
@end
声明我们自己的赋值和取值方法LG_setValue:forKey:
和变量之间的关系LG_valueForKey:
并实现空实现,接下来开始实现具体的自定义方法
3.1、接口英文自定义赋值方法
首先我们来确定自定义赋值方法的流程:
- 验证
<key>
是否存在 - set接口类型ter方法查找:
set<Key>:
和_set<Key>
- 判面试常见问题及回答技巧断
acces变量值sInstanceVaria面试技巧和话术大全blesDirectly
的值,YES
继续NO
则抛出异常 - 查找实例测试工程师变量
_<key>
,_is<key>
,<key>
,is<key>
- 未查找到抛出异常
具体实现如下:
#import "NSObject+LGKVC.h"
#import <objc/runtime.h>
@implementation NSObject (LGKVC)
- (void)LG_setValue:(id)value forKey:(NSString *)key{
// 1、验证`<key>`是否存在
if (key == nil || key.length == 0) {
NSLog(@"key值不存在");
return;
}
// 大写首字母
NSString *Key = key.capitalizedString;
// 2. setter方法查找:`set<Key>:` 和 `_set<Key>`
// 拼接方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
if ([self lg_performSelectorWithMethodName:setKey value:value]) {
return;
}else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
return;
}
// 3. 判断accessInstanceVariablesDirectly的值,YES继续NO则抛出异常
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.查找实例变量_<key>,_is<key>,<key>,is<key>
NSMutableArray *mArray = [self getIvarListName];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
// 获取相应的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 对相应的 ivar 设置值
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}
// 5.未查找到抛出异常
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
- (id)LG_valueForKey:(NSString *)key{
return nil;
}
#pragma mark - 相关方法
//判断方法是否存在并调用
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
NSLog(@"找到方法:%@",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;
}
// 获取实例变量名
- (NSMutableArray *)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;
}
@end
简单测试一下:
LGPerson *p = [LGPerson new];
[p LG_setValue:@"leilei" forKey:@"name"];
运行结果:
lg_performSelectorWit接口是什么hMethodName
并找到setName:
且调用了。后面几个方法查找就不一一验证了。
3.2、自定义取值方法
与赋值方法一样变量类型有哪些,先来确定流程:
- 验证字符常量
<key>测试抑郁症;
是否存在 - getter方法查找:
get<Key>
,<接口是什么key>
,co变量untOf<Key>
,objectIn<Key>A接口和抽象类的区别tIndex
- 判断
accessInstanceVariablesDirectly
的值,YES
继续NO
则抛出异常 - 查找实例变量
_<key>变量名;
,_is<key>
,<key面试问题大全及答案大全>测试用例;
,is<key>
- 未查找到抛出异常
具体实现如下:
- (id)LG_valueForKey:(NSString *)key{
// 1、验证`<key>`是否存在
if (key == nil || key.length == 0) {
return nil;
}
// 大写首字母
NSString *Key = key.capitalizedString;
// 2. setter方法查找:`set<Key>:` 和 `_set<Key>`
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@:",Key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@:",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",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(countOfKey)]){
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
// 3. 判断accessInstanceVariablesDirectly的值,YES继续NO则抛出异常
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.查找实例变量`_<key>`,`_is<key>`,`<key>`,`is<key>`
NSMutableArray *mArray = [self getIvarListName];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
// 5.抛出异常
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
return @"";
}
简单验证一下,代码如下:
LGPerson *p = [LGPerson new];
NSLog(@"取到的值%@",[p valueForKey:@"name"]);
运行结果变量值:
至此一个简单的自定义KVC
就完成了。