KVC原理

KVC是苹果的键值编码技术,全称是Key-Value Coding。键值编码是NSKeyValueCoding非正式协议支持的一种机制,对象采用这种机制来提供对其属性的间接访问。通过KVC能快速获取和设置属性的值。
官方文档的地址

NSObject遵守NSKeyValueCoding协议,所有的继承自NSObject的对象都可以调用KVC方法。

@interface NSObject(NSKeyValueCoding)
...
@end
复制代码

1.用途

1.1访问对象属性

1.1.1通常设置属性是通过setter方法,KVC提供了更灵活的设置方法。如下
[myAccount setCurrentBalance:@(100.0)]

[myAccount setValue:@(100.0) forKey:@"currentBalance"];
复制代码
1.1.2通过键获取属性的API
➤valueForKey: 按照属性查找顺序查找,找到返回结果,找不到报错valueForUndefinedKey:
➤valueForKeyPath: 同上
➤dictionaryWithValuesForKeys:通过键(key)的数组获取对应的键值字典,可用作对象转字典。
例如:
DWZPerson *p1 = [DWZPerson new];
p1.name = @"rose";
p1.hobby = @"play";
NSDictionary *d = [p1 dictionaryWithValuesForKeys:@[@"name",@"hobby"]];
输出的结果是{hobby = play;name = rose;}

➤通过keyPath寻址的时候如果中间有一个值是集合,则返回集合所有后续的路径的值。如下
复制代码

WX20210420-165758.png

1.1.3通过键设置属性的API
➤setValue:forKey: 如果key正确,设置值;不正确,setValue:forUndefinedKey:
➤setValue:forKeyPath:
➤setValuesForKeysWithDictionary: 常用作字典转模型。
复制代码

1.2访问集合属性

mutableArrayValueForKey:和mutableArrayValueForKeyPath:
它们返回行为类似于NSMutableArray对象的代理对象。
mutableSetValueForKey:和mutableSetValueForKeyPath:
它们返回行为类似于NSMutableSet对象的代理对象。
mutableOrderedSetValueForKey:和mutableOrderedSetValueForKeyPath:
它们返回行为类似于NSMutableOrderedSet对象的代理对象。

操作(增加、删除、替换)代理对象,协议的实现会相应的修改基础属性。比使用
valueForKey:获取一个非可变的集合对象,创建一个内容已更改的修改对象,然后使用
setValue:forKey:消息将其存储回该对象更为有效。
复制代码

1.3使用集合操作符

1.3.1组操作符包括:@avg,@count,@max,@min,@sum
//属性amount的平均值
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];

//transactions的条数
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];

//属性date的最大/最小值
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];

//属性amount求和
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
复制代码
1.3.2数组操作符
重要:如果子对象是nil使用数组操作符会导致valueForKeyPath:方法报错。
数组操作符包括:
    @distinctUnionOfObjects 返回不同的对象集合(相当于去重操作)
    NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
    @unionOfObjects 返回所有结果
    NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
复制代码
1.3.3嵌套操作符:对嵌套集合进行操作
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
@distinctUnionOfArrays
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
@unionOfArrays
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
@distinctUnionOfSets
复制代码

1.4KVC在非对象中的表现

KVC能同时作用在OC对象和基本数据结构上,KVC的实现能自动在对象和基本数据结构上转换。如果设置nil给非对象属性,就会调用setNilValueForKey:方法,没有重写该方法就会报错。

1.4.1基本数据类型都是转换NSNumber,例如:BOOL,char,int,double,float,long,short
[p1 setValue:@(20) forKey:@"age"];
[p1 setValue:@(true) forKey:@"isfemale"];
BOOL v3 = [p1 valueForKey:@"isfemale"];
NSNumber *v4 = [p1 valueForKey:@"age"];
复制代码
1.4.2 结构体类型都是转换成NSValue,例如NSPoint, NSRange, NSRect, NSSize

对于自定义的结构体,使用如下:

typedef struct {
    float x, y, z;
} ThreeFloats;

@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end

NSValue* result = [myClass valueForKey:@"threeFloats"];
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];
复制代码

1.5验证属性

//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
复制代码

2 KVC原理

2.1 get方法的查找顺序

1 get<Key>, <key>, is<Key>, _<key> 查找顺序,找到跳到第5步
2 判断是否包含countOf<Key>,objectIn<Key>AtIndex:,<key>AtIndexes: 如果包含第一个方法和后两个方法中的一个就确定是数组,创建一个NSArray对象并返回;否则到第3步。
3判断是否同时包含countOf <Key>,enumeratorOf <Key>,memberOf <Key>:方法,包含就判断是NSSet,创建NSSet对象并返回,否则跳到第4步.
4 accessInstanceVariablesDirectly 判断返回值,为YES,按顺序查找实例变量_<key>, _is<Key>, <key>,  is<Key>找到跳第5步;否则到第6步。
5 如果查找到的值是对象指针,返回结果;如果是基本数据类型,转换成NSNumber并返回;如果不是基本数据类型,转换成NSValue并返回。
6 所有方法都失败,调用valueForUndefinedKey:方法,如果该类的继承链上没实现该方法,就抛出错误。
复制代码

KVC_get.png

2.2 set的方法查找顺序

1 设值顺序:set<Key>,_set<Key>,setIs<Key>,找到就调用方法并返回
2 accessInstanceVariablesDirectly 判断返回值,为YES,设值实例变量并返回;为NO,到第4步。
3 设值实例变量的顺序:_<key>, _is<Key>, <key>,  is<Key>,有相应的实例变量则设置并返回,没有找到的话到第4步。
4 调用setValue:forUndefinedKey:方法,继承链没实现方法就抛出错误。 
复制代码

KVC_set.png

3 自定义KVO

3.1根据原理自定义GET方法

/// 自定义KVCGet方法
/// @param key key
- (nullable id)dwz_GETValueForKey:(NSString *)key{

    // 1:刷选key 判断非空
    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:判断是否能够直接赋值实例变量
    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.找相关实例变量进行赋值
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // _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 @"";
}
复制代码

3.2根据原理自定义SET方法

/// 自定义KVC方法
/// @param value key
/// @param key value
-(void)dwz_SetValue:(NSString *)value forKey:(NSString *)key {
    // 1.判断key是否有值
    if (key == nil || key.length == 0) {
        NSLog(@"dwz_SetValue error with key is nil");
        return;
    }
    // 2.判断是否有setter方法
    NSString *k = key.capitalizedString;
    NSString *setKey = [NSString stringWithFormat:@"set%@:",k];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",k];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",k];

    // setKey方法调用
    if ([self dwz_PerformSelectorWithMethodName:setKey value:value]) {
        NSLog(@"%s -%@",__func__,setKey);
        return;
    }
    // _setKey方法调用
    if ([self dwz_PerformSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"%s -%@",__func__,_setKey);
        return;
    }
    // _setIsKey方法调用
    if ([self dwz_PerformSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"%s -%@",__func__,setIsKey);
        return;
    }
    // 3.判断是否允许直接访问成员变量
    if (![[self class] accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"DWZUnknownKeyException" reason:[NSString stringWithFormat:@"DWZ [%@ setValueForUndefineKey:]%@",self,key] userInfo:nil];
        return;
    }
    // 4.依次判断是否有成员变量并设置相应的值
    NSMutableArray *ivars = [self getIvarListName];

    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",k];
    NSString *isKey = [NSString stringWithFormat:@"is%@",k];
    if ([ivars containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        object_setIvar(self, ivar, value);
        return;
    }
    if ([ivars containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        object_setIvar(self, ivar, value);
        return;
    }
    if ([ivars containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        object_setIvar(self, ivar, value);
        return;
    }
    if ([ivars containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        object_setIvar(self, ivar, value);
        return;
    }
    // 此处是这样处理,正确的应该是调用setValueForUndefineKey:方法
    @throw [NSException exceptionWithName:@"DWZUnknownKeyException" reason:[NSString stringWithFormat:@"DWZ [%@ %@]%@",self,NSStringFromSelector(_cmd),key] userInfo:nil];

}
复制代码

4. 使用场景和注意

4.1 字典和模型的互转

 //字典转模型
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
//模型转字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
复制代码

4.2 设置隐藏的属性值

 例如UITextField的 placeHolderAttributeText 
复制代码

4.3 操作集合数据

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

2021-04-27 17:50:47.982831+0800 KVC[1678:58128] English
2021-04-27 17:50:47.983082+0800 KVC[1678:58128] Franch
2021-04-27 17:50:47.983330+0800 KVC[1678:58128] Chinese
2021-04-27 17:50:47.985906+0800 KVC[1678:58128] 7
2021-04-27 17:50:47.986096+0800 KVC[1678:58128] 6
2021-04-27 17:50:47.986247+0800 KVC[1678:58128] 7
复制代码

4.3 实现setValueForUndefineKey: 和 setNilValueForKey: 方法,防止设值异常崩溃。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享