KVC原理分析

2021-03-25 15:02:09 浏览数 (1)

之前我写过两篇文章详细介绍过KVC的用法:

KVC详解(上)

KVC详解(下)

这篇文章是在上述两篇文章的基础上做一个原理性的补充。

首先呢,我们来聊一聊苹果开发文档。不知道诸位在平时开发中是否有查阅苹果官方文档的习惯,反正我在遇到一些不太清楚的地方的时候首先会去官方文档上看看是否能找到对应的说明,如果没找到,再去百度或者Google。

实际上,博客、掘金或者简书上的很多权威的文章都是翻译的官方文档,甚至连案例都是一模一样的。我曾在项目中做过苹果APNs的改造和优化,很多细节点在网上找的资料都是一会这样一会儿那样,最后还是通过通读苹果APNs的官方文档才明确。

现在我就以查找KVC官方文档为例,演示一下如何在苹果官方文档中找到想要的内容

第一步,打开如下网址:

代码语言:javascript复制
https://developer.apple.com/library/archive/navigation/

第二步,搜索关键词:

这样,就找到对应的文档内容了:

KVC使用补充

修改不可变数组中的元素

Norman类中的terchers属性是一个不可变数组:

代码语言:javascript复制
@interface Norman : NSObject

//这是一个不可变数组
@property (nonatomic, copy)NSArray *terchers;

@end

现在我要修改norman.terchers中的第一个元素,如下:

代码语言:javascript复制
Norman *norman = [Norman new];
norman.terchers = @[@"1", @"2", @"3"];

// 方式一:先复制一个可变数组进行修改,然后将修改后的数组赋值给原数组
NSMutableArray *muArr = norman.terchers.mutableCopy;
muArr[0] = @"10";
norman.terchers = muArr.copy;
NSLog(@"%@", norman.terchers);

// 方式二:使用KVC-mutableArrayValueForKey
NSMutableArray *muTeachers = [norman mutableArrayValueForKey:@"terchers"];
muTeachers[0] = @"100";
NSLog(@"%@", norman.terchers);

打印结果如下:

2021-03-14 11:31:06.367325 0800 Test[1207:73184] (

10,

2,

3

)

2021-03-14 11:31:06.367681 0800 Test[1207:73184] (

100,

2,

3

)

通过打印结果可以看出,通过KVC的方式也是有效的。

通过KVC来存取自定义结构体

LavieStruct是一个自定义的结构体,Norman类中定义了一个LavieStruct类型的属性:

代码语言:javascript复制
typedef struct {
    NSString *name;
    NSInteger age;
} LavieStruct;

@interface Norman : NSObject

@property (nonatomic, assign)LavieStruct lavie;

@end

外界通过KVC给该自定义类型的结构体变量赋值、取值的代码如下:

代码语言:javascript复制
LavieStruct lavie = {
    @"lavie",
    18
};

//通过KVC设置自定义结构体的值(需要将结构体转成NSValue)
Norman *norman = Norman.alloc.init;
[norman setValue:[NSValue value:&lavie withObjCType:@encode(LavieStruct)] forKey:@"lavie"];

//通过KVC读取自定义结构体的值(获取到的是NSValue类型,需要自己转成自定义结构体类型)
NSValue *laVieValue = [norman valueForKey:@"lavie"];
LavieStruct lavie666;
[laVieValue getValue:&lavie666];
NSLog(@"%@, %ld", lavie666.name, (long)lavie.age);

运行之后打印如下:

2021-03-14 12:09:00.082876 0800 Test[1548:126086] lavie, 18

KVC的自动类型转换

KVC有自动转换类型的功能。

现在有这样一个Entity类:

代码语言:javascript复制
@interface Entity : NSObject

@property (nonatomic, copy) NSString *str;

@property (nonatomic, assign) int i1;
@property (nonatomic, assign) int i2;
@property (nonatomic, assign) int i3;

@property (nonatomic, assign) float f1;

@property (nonatomic, strong) NSNumber *num;

@property (nonatomic, assign) BOOL b1;
@property (nonatomic, assign) BOOL b2;
@property (nonatomic, assign) BOOL b3;

@property (nonatomic, strong) NSDate *date1;

@property (nonatomic, assign) NSTimeInterval t1;

@end

外界使用如下。

属性是基本数据类型,使用KVC给该属性赋值,所赋的值是字符串,此时会将所赋值转成NSNumber进行存储

代码语言:javascript复制
// 数值的字符串可以转成数值类型
[obj setValue:@"2.4" forKey:@"i1"];
NSLog(@"%@, %@",[obj valueForKey:@"i1"], NSStringFromClass([[obj valueForKey:@"i1"] class]));
// 非数值的字串不会识别,直接转成0
[obj setValue:@"a" forKey:@"i2"];
NSLog(@"%@, %@",[obj valueForKey:@"i2"], NSStringFromClass([[obj valueForKey:@"i2"] class]));
// 只会识别第一个非数字字符(除0123456789.之外的字符)之前的数字
[obj setValue:@"2014 10-24" forKey:@"i3"];
NSLog(@"%@, %@",[obj valueForKey:@"i3"], NSStringFromClass([[obj valueForKey:@"i3"] class]));
[obj setValue:@"2014-10-24" forKey:@"t1"];
NSLog(@"%@, %@",[obj valueForKey:@"t1"], NSStringFromClass([[obj valueForKey:@"t1"] class]));

打印结果如下:

2021-03-14 17:18:54.824901 0800 Test[3475:481793] 2, __NSCFNumber

2021-03-14 17:18:54.825158 0800 Test[3475:481793] 0, __NSCFNumber

2021-03-14 17:18:54.825302 0800 Test[3475:481793] 2014, __NSCFNumber

2021-03-14 17:18:54.825472 0800 Test[3475:481793] 2014, __NSCFNumber

属性是字符串类型,使用KVC给该属性赋值,所赋的值是NSNumber,此时会直接以NSNumber类型进行存储

代码语言:javascript复制
[obj setValue:@1 forKey:@"str"];
NSLog(@"%@, %@",[obj valueForKey:@"str"], NSStringFromClass([[obj valueForKey:@"str"] class]));

打印结果如下:

2021-03-14 17:18:54.825605 0800 Test[3475:481793] 1, __NSCFNumber

属性是基本数据类型,使用KVC给该属性赋值,所赋的值是NSNumber,此时会直接以NSNumber类型进行存储

代码语言:javascript复制
[obj setValue:@1.23 forKey:@"f1"];
NSLog(@"%@, %@",[obj valueForKey:@"f1"], NSStringFromClass([[obj valueForKey:@"f1"] class]));

打印结果如下:

2021-03-14 17:18:54.825739 0800 Test[3475:481793] 1.23, __NSCFNumber

属性是字符串类型,使用KVC给该属性赋值,所赋的值是字符串,此时会直接以NSString类型进行存储

代码语言:javascript复制
[obj setValue:@"99" forKey:@"num"];
NSLog(@"%@, %@",[obj valueForKey:@"num"], NSStringFromClass([[obj valueForKey:@"num"] class]));

打印如下:

2021-03-14 17:18:54.825862 0800 Test[3475:481793] 99, __NSCFConstantString

属性是Bool类型,使用KVC给该属性赋值,所赋的值是字符串,此时会以NSCFBoolean类型进行存储

代码语言:javascript复制
// 对于Number而言,0.0 为假, 其余均为真
[obj setValue:@0.1 forKey:@"b1"];
NSLog(@"%@, %@",[obj valueForKey:@"b1"], NSStringFromClass([[obj valueForKey:@"b1"] class]));
// 对于字符串而言,绝对值 >= 1.0 为真, 1.0以下为假,因为字符串会先转成Int然后再转成Number
[obj setValue:@"-1.1" forKey:@"b2"];
NSLog(@"%@, %@",[obj valueForKey:@"b2"], NSStringFromClass([[obj valueForKey:@"b2"] class]));
// true TRUE yes YES false FALSE no NO 都可以识别
[obj setValue:@"false" forKey:@"b3"];
NSLog(@"%@, %@",[obj valueForKey:@"b3"], NSStringFromClass([[obj valueForKey:@"b3"] class]));

打印如下:

2021-03-14 17:18:54.826026 0800 Test[3475:481793] 1, __NSCFBoolean

2021-03-14 17:18:54.826616 0800 Test[3475:481793] 1, __NSCFBoolean

2021-03-14 17:18:54.827003 0800 Test[3475:481793] 0, __NSCFBoolean

属性是NSDate类型,使用KVC给该属性赋值,所赋的值是字符串,此时会以NSString类型进行存储

代码语言:javascript复制
[obj setValue:@"2014-10-24" forKey:@"date1"];
NSLog(@"%@, %@",[obj valueForKey:@"date1"], NSStringFromClass([[obj valueForKey:@"date1"] class]));

打印如下:

2021-03-14 17:16:23.957665 0800 Test[3437:476790] 2014-10-24, __NSCFConstantString

总结如下:

给数值类型的属性进行赋值,无论所赋值是什么类型,最后都会转成Number类型存储;

给布尔类型的属性进行赋值,最终都会以布尔类型存储;

给对象类型的属性进行赋值,所赋值是什么类型,最终就以什么类型进行存储

KVC的底层原理探究

我们知道,可以通过KVC来对对象的属性进行动态地赋取值,那么其内部是如何实现的呢?

实际上,在官方文档中已经解释的很清楚了:

KVC设值:

KVC取值:

实际上,关于KVC设置与取值的过程,我在KVC详解(上)中有过详细说明,不过之前的文章中有些地方有些遗漏,我在这里补充说明下。

上图是之前文章中总结的KVC设值的流程,其中第一步中的setter方法有两个,先走setKey方法,没有的话再走_setKey方法,都没有的话就进入上图中的第2步

上图是之前文章中总结的KVC取值的过程,标红的第一步是先走的getter方法,之前值写了三个,实际上是有4个,按照-getKey、-key、-isKey、-_key的顺序查找getter方法

自定义KVC

苹果只给我们提供了KVC的文档,并没有将KVC的源码开源出来。因此,为了加深理解,我按照官方文档的步骤说明,自定义了一个简易版的KVC。

该自定义的KVC是通过给NSObject类添加一个NormanKVC分类来实现的:

通过KVC详解(上)中的介绍我们也知道了,苹果的KVC源码也是通过给NSObject等类添加类目的形式来实现的。

作为一名高级开发人员,一定要善于使用类目来分离代码,这样的好处一是可以降低代码的臃肿性,二是可以在多人开发的时候减少代码冲突,第三个好处就是解耦合

具体实现代码如下:

代码语言:javascript复制
#import "NSObject NormanKVC.h"
#import <objc/runtime.h>

@implementation NSObject (NormanKVC)

/// 自定义KVC设值过程
- (void)lv_setValue:(nullable id)value forKey:(NSString *)key {

    // 1,非空判断一下
    if (key == nil  || key.length == 0) return;

    // 2,查找相关Setter方法:set<Key>、_set<Key>
    NSString *Key = key.capitalizedString; // key首字母大写->Key
    // 拼接Setter方法名
    NSString *setKey = [NSString stringWithFormat:@"set%@:", Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key];

    if ([self lv_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********", setKey);
        return;
    } else if ([self lv_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********", _setKey);
        return;
    }

    // 3,判断是否能够直接赋值实例变量,如果不能,则抛出异常
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LVUnknownKeyException" 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:@"LVUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

/// 自定义KVC取值过程
- (nullable id)lv_valueForKey:(NSString *)key {
    // 1,刷选key 判断非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2,找到相关Getter方法:get<Key>、<key>、countOf<Key>、objectIn<Key>AtIndex
    NSString *Key = key.capitalizedString; // key首字母大写->Key
    // 拼接Getter方法名
    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:@"LVUnknownKeyException" 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 @"";
}

#pragma mark - Private Methods

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

@end

以上。

0 人点赞