017*:kvc原理 :(赋值和取值【_key,_iskey,key,iskey】(【路由、模型转换、私有变量】)
问题
目录
1、定义
2、KVC 相关方法
3、赋值的策略
预备
1: 定义基本的方法
@interface LPPerson : NSObject{ @public NSString *_isName; NSString *name; NSString *isName; NSString *_name; } @property (nonatomic, strong) NSArray *arr; @property (nonatomic, strong) NSSet *set; @end @implementation LPPerson #pragma mark - 关闭或开启实例变量赋值 + (BOOL)accessInstanceVariablesDirectly{ return YES; } //MARK: - setKey. 的流程分析 - (void)setName:(NSString *)name{ NSLog(@"%s - %@",__func__,name); } - (void)_setName:(NSString *)name{ NSLog(@"%s - %@",__func__,name); } - (void)setIsName:(NSString *)name{ NSLog(@"%s - %@",__func__,name); } - (void)_setIsName:(NSString *)name{ NSLog(@"%s - %@",__func__,name); } //MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>, - (NSString *)getName{ return NSStringFromSelector(_cmd); } - (NSString *)name{ return NSStringFromSelector(_cmd); } - (NSString *)isName{ return NSStringFromSelector(_cmd); } - (NSString *)_name{ return NSStringFromSelector(_cmd); } @end
2:赋值和取值的过程
@interface LPPerson : NSObject{ @public // NSString *_isName; // NSString *name; NSString *isName; // NSString *_name; } @property (nonatomic, strong) NSArray *arr; @property (nonatomic, strong) NSSet *set; @end - (void)viewDidLoad { [super viewDidLoad]; LPPerson *person = [[LPPerson alloc] init]; // 1: KVC - 设置值的过程 setValue 分析调用过程 [person setValue:@"peter" forKey:@"name"]; // setter - getter - KVC 设值 和 取值的流程 // 2: KVC - 取值的过程 // person->_name = @"_name = peter"; // person->_isName = @"_isName = peter"; // person->name = @"name = peter"; person->isName = @"isName = peter"; NSLog(@"取值:%@",[person valueForKey:@"name"]); }
正文
1、定义
1.1:KVC(全称key-value coding)键值编码。在iOS开发中,允许开发者通过key直接访问对象的属性,或者给对象的属性进行赋值,而不需要调用明确的存取方法。
这样就可以在运行时动态的访问和修改对象的属性,而不是在编译时确定。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。
1.2:KVC的定义是通过对NSObject的扩展来实现的,定义在 NSKeyValueCoding.h
文件中,是一个非正式协议。
2、KVC 相关方法
// 通过key来取值 - (id)valueForKey:(NSString *)key; // 通过keyPath来取值 - (id)valueForKeyPath:(NSString *)keyPath; // 通过key来设值 - (void)setValue:(id)value forKey:(NSString *)key; // 通过keyPath来设值 - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
NSKeyValueCoding
中还有其他的相关方法,例如:
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索 + (BOOL)accessInstanceVariablesDirectly; //KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。 - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; //这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。 - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; //如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。 - (nullable id)valueForUndefinedKey:(NSString *)key; //和上一个方法一样,但这个方法是设值。 - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; //如果你在SetValue方法时面给Value传nil,则会调用这个方法 - (void)setNilValueForKey:(NSString *)key; //输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
3、赋值的策略
1:研究setValue:forKey:赋值的底层实现
调用setValue:forKey:方法的时候,底层调用又分为一下几个步骤
- 1.首先查找是否有setKey,_setKey,_setIsKey(
这三个set方法是一次查找,有一个实现就进行赋值,不再向下进行查找
)。其中key是指成员变量。如果都没有则进行第2步。 - 2.如果第1步没找到写的3种set方法,则去查找accessInstanceVariablesDirectly(可直接返回的实例变量)是否返回YES(
默认返回YES
)。如果返回的是YES,表示如果没有找到SetKey方法的话,会查找直接访问的实例变量进行赋值,会按照_key,_iskey,key,iskey的顺序搜索成员(Key指成员变量),设置成NO就不这样搜索
。如果返回的是NO或者没有找到上面的方法,则进行第3步。 - 3.来到这一步说明
setter方法和实例变量都没有找到,系统就会执行该对象的setValue:forUndefinedKey: 方法
,如果该方法没有实现,则会抛出NSUndefinedKeyException类型异常
(方法setValue:forUndefinedKey:是需要我们自己实现)。
综上所述,KVC通过setValue:forKey:方法设置值的流程如下图所示(已设置Person类的name为例):
4:KVC 取值 底层原理
- 首先按
get<Key>
、<key>
、is<Key>
的顺序查找getter
方法,找到直接调用。
- 若方法的返回结果类型是一个对象指针,则直接返回结果。
- 若类型为能够转化为
NSNumber
的基本数据类型,转换为NSNumber
后返回;否则转换为NSValue
返回。
- 若上面的
getter
没有找到,则查找countOf<Key>
、objectIn<Key>AtIndex:
、<Key>AtIndexes
格式的方法。如果countOf<Key>
和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray
所有方法的集合代理。发送给这个代理集合的NSArray
消息方法,就会以countOf<Key>
、objectIn<Key>AtIndex:
、<Key>AtIndexes
这几个方法组合的形式调用。如果receiver
的类实现了get<Key>:range:
方法,该方法也会用于性能优化。 - 还没查到,那么查找
countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
格式的方法。如果这3个方法都找到,那么久返回一个可以相应NSSet所有方法的集合代理。发送给这个代理集合的NSSet消息方法,就会以countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
组合的形式调用。 - 还是没查到,那么如果类方法
accessInstanceVariablesDirectly
返回YES,那么按_<key>
、_is<Key>
、<key>
、is<Key>
的顺序直接搜索实例变量。如果搜索到了,则返回receiver相应实例变量的值。 - 再没有查到,调用
valueForUndefinedKey:
方法,抛出异常。
5、使用keyPath
在实际开发过程中,一个类的成员变量有可能是自定义类或者其他的复杂数据类型,我们可以先用KVC获取该属性,然后再用KVC来获取这个自定义类的属性。但这样比较繁琐,因此KVC提供了一个解决方案,keyPath。
// 通过KeyPath来取值 - (nullable id)valueForKeyPath:(NSString *)keyPath; // 通过KeyPath来设值 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
使用KVC过程中最常见的异常就是不小心使用了错误的key,或者在设值时不小心传了nil的值,KVC有特定的方法处理这些异常。
- KVC处理nil异常,如果在设值过程中,不小心传了nil值,KVC会调用方法
setNilValueForKey:
,这个默认方法是抛出NSInvalidArgumentException
异常,所以一般而言最好重写这个方法,对异常进行处理。 - KVC处理UndefinedKey异常,如果在设值取值传的key不存在时,程序就会crash,设值会调用到
setValue:forUndefinedKey:
方法,而取值会调用valueForUndefinedKey:
方法,这两个方法默认都是抛出NSUndefinedKeyException
异常,因此如果要避免程序crash,可以重写这两个方法。
7:自定义kvc
1:自定义KVC取值
自定义KVC设置流程,主要分为以下几个步骤:
- 1、判断
key非空
- 2、查找
setter
方法,顺序是:setXXX、_setXXX、 setIsXXX
- 3、判断是否响应
accessInstanceVariablesDirectly
方法,即间接访问实例变量,- 返回
YES
,继续下一步设值, - 如果是
NO
,则崩溃
- 返回
- 4、间接访问变量赋值(只会走一次),顺序是:
_key、_isKey、key、isKey
- 4.1 定义一个收集实例变量的可变数组
- 4.2 通过
class_getInstanceVariable
方法,获取相应的 ivar - 4.3 通过
object_setIvar
方法,对相应的 ivar 设置值
- 5、如果找不到相关实例变量,则抛出异常
//设值 - (void)cjl_setValue:(nullable id)value forKey:(NSString *)key{ // 1、判断key 是否存在 if (key == nil || key.length == 0) return; // 2、找setter方法,顺序是:setXXX、_setXXX、 setIsXXX // key 要大写 NSString *Key = key.capitalizedString; // key 要大写 NSString *setKey = [NSString stringWithFormat:@"set%@:", Key]; NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key]; NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key]; if ([self cjl_performSelectorWithMethodName:setKey value:value]) { NSLog(@"*************%@*************", setKey); return; }else if([self cjl_performSelectorWithMethodName:_setKey value:value]){ NSLog(@"*************%@*************", _setKey); return; }else if([self cjl_performSelectorWithMethodName:setIsKey value:value]){ NSLog(@"*************%@*************", setIsKey); return; } // 3、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃 if (![self.class accessInstanceVariablesDirectly]) { @throw [NSException exceptionWithName:@"CJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil]; } // 4、间接访问变量赋值,顺序为:_key、_isKey、key、isKey // 4.1 定义一个收集实例变量的可变数组 NSMutableArray *mArray = [self getIvarListName]; // _<key> _is<Key> <key> is<Key> NSString *_key = [NSString stringWithFormat:@"_%@", key]; NSString *_isKey = [NSString stringWithFormat:@"_is%@", key]; NSString *isKey = [NSString stringWithFormat:@"is%@", key]; if ([mArray containsObject:_key]) { // 4.2 获取相应的 ivar Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String); // 4.3 对相应的 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:@"CJLUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil]; }
2:自定义KVC取值
取值的自定义代码如下,分为以下几步
-
1、判断
key非空
-
2、查找相应方法,顺序是:
get<Key>、 <key>、 countOf<Key>、 objectIn<Key>AtIndex
-
3、判断是否能够直接赋值实例变量,即判断是否响应
accessInstanceVariablesDirectly
方法,间接访问实例变量,-
返回
YES
,继续下一步取值 -
如果是
NO
,则崩溃
-
-
4、间接访问实例变量,顺序是
:_<key> _is<Key> <key> is<Key>
-
4.1 定义一个收集实例变量的
可变数组
-
4.2 通过
class_getInstanceVariable
方法,获取相应的 ivar -
4.3 通过
object_getIvar
方法,返回相应的 ivar 的值
-
//取值 - (nullable id)cjl_valueForKey:(NSString *)key{ // 1、判断非空 if (key == nil || key.length == 0) { return nil; } // 2、找到相关方法:get<Key> <key> countOf<Key> objectIn<Key>AtIndex // key 要大写 NSString *Key = key.capitalizedString; // 拼接方法 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:@"CJLUnKnownKeyException" 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> // 4.1 定义一个收集实例变量的可变数组 NSMutableArray *mArray = [self getIvarListName]; // 例如:_name -> _isName -> name -> isName 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);; } return @""; return @""; }
8:KVC 使用场景
1、动态设值和取值
-
常用的可以通过
setValue:forKey:
和valueForKey:
-
也可以通过
路由
的方式setValue:forKeyPath:
和valueForKeyPath:
2、通过KVC访问和修改私有变量
在日常开发中,对于类的私有属性
,在外部定义的对象,是无法直接访问私有属性的,但是对于KVC而言,一个对象没有自己的隐私
,所以可以通过KVC修改和访问任何私有属性
3、多值操作(model和字典互转)
model和字典的转换可以通过下面两个KVC的API实现
//字典转模型 - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues; //模型转字典 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
4、修改一些系统空间的内部属性
在日常开发中,我们知道,很多UI控件都是在其内部由多个UI空间组合而成,这些内部控件苹果并没有提供访问的API,但是使用KVC可以解决这个问题,常用的就是自定义tabbar
、个性化UITextField
中的placeHolderText
5、用KVC实现高阶消息传递
在对容器类使用KVC
时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合 来返回 另一个集合
//KVC实现高阶消息传递 - (void)transmitMsg{ NSArray *arrStr = @[@"english", @"franch", @"chinese"]; NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"]; for (NSString *str in arrCapStr) { NSLog(@"%@", str); } NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"]; for (NSNumber *length in arrCapStrLength) { NSLog(@"%ld", (long)length.integerValue); } } //********打印结果******** 2020-10-27 11:33:43.377672+0800 CJLCustom[60035:6380757] English 2020-10-27 11:33:43.377773+0800 CJLCustom[60035:6380757] Franch 2020-10-27 11:33:43.377860+0800 CJLCustom[60035:6380757] Chinese 2020-10-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 7 2020-10-27 11:33:43.378327+0800 CJLCustom[60035:6380757] 6 2020-10-27 11:33:43.378417+0800 CJLCustom[60035:6380757] 7
6.访问非对象属性
typedef struct { float x, y, z; } ThreeFloats; ThreeFloats floats = {1.,2.,3.}; NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)]; [person setValue:value forKey:@"threeFloats"]; NSValue *value1 = [person valueForKey:@"threeFloats"]; NSLog(@"%@",value1); ThreeFloats th; [value1 getValue:&th]; NSLog(@"%f-%f-%f",th.x,th.y,th.z);
注意