iOS之KVC原理&自定义KVC

9 篇文章 0 订阅
2 篇文章 0 订阅

前言

开发过程中,很多人都会注意到KVO,以及自定义KVO,实际上KVC的作用也是十分强大的,不仅仅是简单的字典转模型,有关使用技巧可以看上篇文章,这篇文章要根据上篇的总结来进行自定义KVC操作;

相关代码:KVCCode(上篇代码也在这里)

KVC原理

实际在自定义过程中主要要注意的2大点:1.KVC设置过程,2.KVC取值过程,

1.KVC赋值过程

1:非空判断一下

2:找到相关方法set<Key>,_set<Key>,_setIs<Key>实例方法进行赋值

3:判断是否能够直接赋值实例变量判断,即accessInstanceVariablesDirectly,且返回值为YES;

        3.1:找相关实例变量进行赋值

             3.1.1 定义一个收集实例变量的可变数组

             3.1.2 获取相应的 ivar

             3.1.3 对相应的 ivar 设置值

4.如果找不到相关实例setValue:forUndefinedKey报出异常

- (void)xz_setValue:(nullable id)value forKey:(NSString *)key{
    
    // 1:非空判断一下
    if (key == nil  || key.length == 0) return;
    
    // 2:找到相关方法 set<Key> _set<Key> setIs<Key>
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self xz_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self xz_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self xz_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"XZUnknownKeyException" 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>
    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:@"XZUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
#pragma mark - 方法分发
- (BOOL)xz_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;
}

- (id)performSelectorWithMethodName:(NSString *)methodName{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}

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

 

2.KVC取值过程(ValueForKey:)

1.对key 判断非空

2.找到相关方法getKey, key, isKey, _key,

3:判断是否能够直接赋值实例变量是否实现类方法accessInstanceVariablesDirectly

4..按照 _key,_iskey,key,isKey 顺序查询实例变量

5. 抛出异常ValueForUndefinedKey 报错

- (nullable id)xz_valueForKey:(NSString *)key{
    
    // 1:刷选key 判断非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相关方法 getKey, key, isKey, _key
    // 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:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"XZUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
     // 4.按照 _key,_iskey,key,isKey 顺序查询实例变量
    NSMutableArray *mArray = [self getIvarListName];
    _key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    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.抛出异常
    @throw [NSException exceptionWithName:@"XZUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: valueForUndefinedKey:%@.****",self,NSStringFromSelector(_cmd),key] userInfo:nil];

    return @"";
}

取值过程的自定义也结束了,其实这里也有不严谨的地方,比如取得属性值返回的时候需要根据属性值类型来判断是否要转换成 NSNumber 或 NSValue,以及对 NSArray 和 NSSet 类型的判断。在KVCCode(中有个写的比较牛逼的KVC代码,有兴趣可以下载下来看看)

KVC异常小技巧

下面代码会使用XZPerson类

NS_ASSUME_NONNULL_BEGIN

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

@interface XZPerson : NSObject{
    @public
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
    
}

@property (nonatomic, copy) NSString *subject;
@property (nonatomic, assign) int  age;
@property (nonatomic, assign) BOOL sex;
@property (nonatomic) ThreeFloats  threeFloats;

@end

NS_ASSUME_NONNULL_END

1: KVC 自动转换类型

看下面代码我们在给age赋值的时候一般情况会不能直接复制int类型,会使用下面方式

    XZPerson *person = [[XZPerson alloc] init];

    [person setValue:@18 forKey:@"age"];
    NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber

    // 上面那个表达 大家应该都会! 但是下面这样操作可以?
    [person setValue:@"20" forKey:@"age"]; // int - string
    NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber

看一下输出结果:

上面使用@18 输出的是__NSCFNumber(类簇,属于NSNumber的子类) 是可以理解的,但是 @“20”也是__NSCFnumber ,这说明在赋值过程会进行对应的类型转换

同样的类型转换,在结构体中也会出现

    
XZPerson *person = [[XZPerson alloc] init];
    
[person setValue:@"20" forKey:@"sex"];
    
NSLog(@"%@-%@",[person valueForKey:@"sex"],[[person valueForKey:@"sex"] class]);//__NSCFBoolean

 ThreeFloats floats = {1., 2., 3.};
    NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSLog(@"%@-%@",[person valueForKey:@"threeFloats"],[[person valueForKey:@"threeFloats"] class]);//NSConcreteValue

这里的输出过为:

bool 类型会转换为__NSCFBoolean(NSCFNumber) ,结构体会转换为NSConcreteValue (NSValue)类型

2: 设置空值(setNilValueForKey实现方法进行容错提示)

我们可以对age,sex进行设置空置

    
XZPerson *person = [[XZPerson alloc] init];
NSLog(@"******2: 设置空值******");

[person setValue:nil forKey:@"age"]; // subject不会走 - 官方注释里面说只对 NSNumber - NSValue

我们可以在person类中实现setNilValueForKey方法进行监控(注:如果没有实现这个方法会导致崩溃),进行容错提示

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"你傻不傻: 设置 %@ 是空值",key);
}

在setNilValueForKey方法的注释文档中描述如下,描述了,可以监控到NSNumber,和NSValue ,是监听不到NSString类型的

/* Given that an invocation of -setValue:forKey: would be unable to set the keyed value because the type of the parameter of the corresponding accessor method is an NSNumber scalar type or NSValue structure type but the value is nil, set the keyed value using some other mechanism. The default implementation of this method raises an NSInvalidArgumentException. You can override it to map nil values to something meaningful in the context of your application.
*/

3: 插入找不到的 key(setValue: forUndefinedKey 进行容错)

如果我们给person中插入一个不存在的属性

    NSLog(@"******3: 找不到的 key******");
    [person setValue:nil forKey:@"Alan"];

这个时候如果直接运行就会报错,找不到这个key,可以添加setValue: forUndefinedKey:进行容错提示

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{

    NSLog(@"你瞎啊: %@ 没有这个key",key);

}

 4: 取值时 - 找不到 key(valueForUndefinedKey:)

取值时去一个不包含的属性,进行容错处理

    // 4: 取值时 - 找不到 key
    NSLog(@"******4: 取值时 - 找不到 key******");
    NSLog(@"%@",[person valueForKey:@"Alan"]);

需要person类中添加方法

- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"你瞎啊: %@ 没有这个key - 给你一个其他的吧,别奔溃了!",key);
    return @"Master 牛逼";
}

5: 键值验证

这个在开发中用的相对来说较少,主要是封装一些库,可能不想让上层了解具体属性是怎么进行操作的,才会有这种操作:具体如下:给person职工插入names属性进行验证,如过有错误就报错,如果没有错误,输出names和subject值

    NSLog(@"******5: 键值验证******");
    NSError *error;
    NSString *name = @"Alan";
    if (![person validateValue:&name forKey:@"names" error:&error]) {
        NSLog(@"%@",error);
    }else{
        NSLog(@"%@",[person valueForKey:@"subject"]);
    }
    if (![person validateValue:&name forKey:@"alan" error:&error]) {
        NSLog(@"%@",error);
    }else{
        NSLog(@"%@",[person valueForKey:@"subject"]);
    }

如果要实现重定向就需要在person类中实现:

代码逻辑为,如果传入的key值为names的话,就传入的值进行修改拼接了字符,并且存储到subject属性中,如果不是names属性就直接抛出错误

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError{
    if([inKey isEqualToString:@"names"]){
        [self setValue:[NSString stringWithFormat:@"里面修改一下: %@",*ioValue] forKey:@"subject"];
        return YES;
    }
    *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:10088 userInfo:nil];
    return NO;
}

输出日志信息:

 

总结

KVC 探索完了,其实我们探索的大部分内容都是基于苹果的官方文档,我们在探索 iOS 底层的时候,文档思维十分重要,有时候说不定在文档的某个角落里就隐藏着追寻的答案。KVC 用起来不难,理解起来也不难,但是这不意味着我们可以轻视它。在 iOS 13 之前,我们可以通过 KVC 去获取和设置系统的私有属性,但从 iOS 13 之后,这种方式被禁用掉了。建议对 KVC 理解还不透彻的读者去多几遍官方文档,相信我,你会有新的收获。最后,我们简单总结一下本文的内容。

  • KVC 是一种 NSKeyValueCoding 隐式协议所提供的机制。
  • KVC 通过 valueForKey:valueForKeyPath: 来取值,不考虑集合类型的话具体的取值过程如下:
    • get<Key>, <key>, is<Key>, _<key> 的顺序查找方法
    • 如果找不到方法,则通过类方法 accessInstanceVariablesDirectly 判断是否能读取成员变量来返回属性值
    • _<key>, _is<Key>, <key>, is<Key> 的顺序查找成员变量
  • KVC 通过 setValueForKey:setValueForKeyPath: 来取值,不考虑集合类型的话具体的设置值过程如下:
    • set<Key>, _set<Key>的顺序查找方法
    • 如果找不到方法,则通过类方法 accessInstanceVariablesDirectly 判断是否能通过成员变量来返回设置值
    • _<key>, _is<Key>, <key>, is<Key> 的顺序查找成员变量

5种异常处理

  •  KVC 自动转换类型

  •  设置空值容错

  • 插入找不到的 key容错

  •  取值时 - 找不到 key容错

  • 键值验证重定向

希望对大家有用处,欢迎大家点赞+评论,关注我的CSDN,我会定期做一些技术分享!未完待续。。。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值