序言

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);

输出结果:

iOS KVC分析

输出结果是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);

输出结接口测试果:

iOS KVC分析

简单的体验一下KVC。

二、认识KVC

KVC是NSKeyValueCoding非正式协议支持的一种机制,对象采用这种机制来提供对其属性的间接访问。当一个对象符合键值编码时,它的属性可以通过一个简洁、统变量英语一的消息传递接口面试自我介绍一分钟字符串参数寻址。这种间接访问机制补充了实例变量及其关联访问器方法所提供的直接访问。

这段引用自官方文档

那么KVC是面试问题赋值和取值的机制是怎么样的呢?在官方文档里有这么接口是什么一个解释简单翻译一下:

  1. 赋值
  • 查找方法:按照set<Key>:_set<Key>的顺序查找
  • 查找实例变量:判断accessInstanceVariable变量与函数sDirectly返回值为YES,然后按照_<key>,_is<Key>,<key>, 和is<Key>的顺序查找
  1. 取值
  • 查找方法:按照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"];

运行结果:

iOS KVC分析
走了setName:方法,注释此方法再次运行,结果如下:
iOS KVC分析
这一次字符串是什么意思走到了_setName:方法, 验证了赋值的方法查找。

2.1.2、实例变量查找验证

将方法注释了,添加4个实例变量_name_i变量英语sNamenameisName并实现 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);

运行结果:

iOS KVC分析
直接抛出变量类型有哪些异常,说明将ac字符串是什么意思cessInstanceVariablesDirectly的返回值设为NO不支持实例变量的查找,将将accessInstanceVariablesDirectly的返回值设为YES再次运行:

iOS KVC分析
_name有值。注释_name后运行结果:

iOS KVC分析
_isName变量名的命名规则有值。注释_isNam面试问题e后运行结果:

iOS KVC分析
name有值。注释name后运面试问题大全及答案大全行结果:

iOS KVC分析
在只有isName的情况下才会给isName赋值。 至此赋值即setValue:forKey:的验证结束测试英文,得出结论:接口英文

KVC在使用setVa面试问题大全及答案大全lue:forKey:赋值时,先查找set<key>然后查找_set<key>这2个方法,如果未找到则会判断accessInstanc变量名eVariablesDirectly返回值,为NO直接抛出异常,为YES继续查找实例变量,查找顺序为_name_isNamen面试问题ameisName

2.2、取值验证

2.2.1、方法查找验证

首先把LGPerson.h中的方法全部注释,然后在LGPerson.m添加getNamename字符是什么isName_nameaccessInstanceVariablesDirectly返回值为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"]);

运行结果如图:

iOS KVC分析
取到的值是getName,注释该方法继续运行:

iOS KVC分析
取到的值是name,注释该方法继续运变量名行:

iOS KVC分析
取到的值是isName,注释该方法继续运行:

iOS KVC分析
只有_name方法,此时取到的值为_name

2.2接口.2、实例变量查找验证

添加4个实例变量_name_isNamenameisName并实现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"]);

运行结果如下:

iOS KVC分析
同样变量名的命名规则抛出异常,将accessInstan面试自我介绍简单大方ceVariablesDirectly返回值为直接设为YES

iOS KVC分析
取到的值为_name,注释后再次运行:

iOS KVC分析
取到的值为_isName,注释后再次运行:

iOS KVC分析
取到的值为name,注释后再次运行:

iOS KVC分析
最后取到的值为isName。我们验证完毕,得出结论:

KVO取值变量值valueForKey:先查找方法,按照getNamenameisName接口_name面试顺序字符间距怎么加宽进行查找。没接口crc错误计数有找到对应方法则会对accessInstanceVariablesDirectly进行判断,若为NO直接抛出异常,为YES则继续进行实例变量查找,查找顺序为_name_isNamenameisName

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相关的方法和实例变量,运行结果如下:

iOS KVC分析
没有抛出异常,并且走了我们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、接口英文自定义赋值方法

首先我们来确定自定义赋值方法的流程:

  1. 验证<key>是否存在
  2. set接口类型ter方法查找:set<Key>:_set<Key>
  3. 面试常见问题及回答技巧acces变量值sInstanceVaria面试技巧和话术大全blesDirectly的值,YES继续NO则抛出异常
  4. 查找实例测试工程师变量_<key>_is<key><key>is<key>
  5. 未查找到抛出异常

具体实现如下:

#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"];

运行结果:

iOS KVC分析
调用lg_performSelectorWit接口是什么hMethodName并找到setName:且调用了。后面几个方法查找就不一一验证了。

3.2、自定义取值方法

与赋值方法一样变量类型有哪些,先来确定流程:

  1. 验证字符常量<key>测试抑郁症;是否存在
  2. getter方法查找:get<Key><接口是什么key>co变量untOf<Key>objectIn<Key>A接口和抽象类的区别tIndex
  3. 判断accessInstanceVariablesDirectly的值,YES继续NO则抛出异常
  4. 查找实例变量_<key>变量名;_is<key><key面试问题大全及答案大全>测试用例;is<key>
  5. 未查找到抛出异常

具体实现如下:

- (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"]);

运行结果变量值

iOS KVC分析
后面的也不再一一验证。

至此一个简单的自定义KVC就完成了。