美文网首页
KVC底层原理

KVC底层原理

作者: f8d1cf28626a | 来源:发表于2022-06-14 11:55 被阅读0次

    KVC 的全称是 Key-Value Coding,翻译成中文是 键值编码,键值编码是由NSKeyValueCoding 非正式协议启用的一种机制,对象采用该协议来间接访问其属

    性。既可以通过一个字符串 key 来访问某个属性。这种间接访问机制补充了实例变

    量及其相关的访问器方法所提供的直接访问。

    KVC相关API常用方法

    主要有以下四个常用的方法

    - 通过key设值/取值

    key设值&取值

    - 通过keyPath(即路由)设值/取值

    keyPath设值&取值

    其他方法

    ~~~

    //默认返回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;

    ~~~

    KVC 是什么??

    KVC 是 NSObject 的一个分类,也叫键值编码,

    文档描述路径优先

    设置

    setter:set , _set

    设置 key:_<key> , _is<key> , <key> or is<key>

    取值 getter:valueForKey: (get<key>,<key>,is<key> or _<key>)

    ### KVC 设值底层原理

    在日常开发中,针对对象属性的赋值,一般有以下两种方式

    - 直接通过setter方法赋值

    - 通过KVC键值编码的相关API赋值

    ~~~

    LGPerson*person=[[LGPersonalloc]init];

    // 1、一般setter 方法

    person.name=@"RC_哈哈";/

    / 2、KVC方式

    [person setValue:@"RC_嘻嘻"forKey:@"name"];

    ~~~

    下面针对使用最多的 KVC 设值方法:setValue:forKey,来进行其底层原理的探索。

    首先进入 setValue:forKey 的声明,发现是在 Foundation 框架中,而 Foundation 框

    架是不开源的,有以下几种方式可以去探索底层

    - 通过Hopper反汇编,查看伪代码

    - 通过苹果官方文档

    - Github搜索是否有相关的demo

    在这里,我们通过 Key-Value Coding Programming Guide 苹果官方文档来研究,针对设值流程,有如下说明

    当调用

    setValue:forKey:设置属性 value 时,其底层的执行流程为

    - 【第一步】首先查找是否有这三种setter方法,按照查找顺序为set<Key>:-

    > _set<Key> -> setIs<Key>

    - 如果有其中任意一个setter方法,则直接设置属性的value(主注意:key是指

    成员变量名,首字符大小写需要符合 KVC 的命名规范)

    - 如果都没有,则进入【第二步】

    - 【第二步】:如果没有第一步中的三个简单的setter方法,则查找

    accessInstanceVariablesDirectly 是否返回 YES,

    - 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为:

    _<key> -> _is<Key> -> <key> -> is<Key>

    - 如果都没有,则进入【第三步】

    - 如果返回NO,则进入【第三步】

    - 【第三步】如果setter方法 或者 实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出 NSUndefinedKeyException类型的异常

    #### 综上所述:

    KVC 通过 setValue:forKey: 方法设值的流程以设置 LGPerson 的对象person 的属性 name 为例,如下图所示

    ### KVC 取值底层原理

    同样的,我们可以通过官方文档分析 KVC 取值的底层原理

    当调用 valueForKey:时,其底层的执行流程如下

    - 【第一步】首先查找getter方法,按照get<Key> -> <key> -> is<Key> -> _<key> 的方法顺序查找,

    - 如果找到,则进入【第五步】

    - 如果没有找到,则进入【第二步】

    - 【第二步】如果第一步中的getter方法没有找到,KVC会查找countOf<Key>和 objectIn <Key> AtIndex :和<key> AtIndexes :

    - 如果找到countOf<Key>和其他两个中的一个,则会创建一个响应所有NSArray 方法的集合代理对象,并返回该对象,即 NSKeyValueArray,是NSArray 的子类。代理对象随后将接收到的所有 NSArray 消息转换为countOf<Key>,objectIn<Key> AtIndex:和<key>AtIndexes:消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为 get<Key>:range:之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合 KVC 的标准命名方法,包括方法签名。)

    - 如果没有找到这三个访问数组的,请继续进入【第三步】

    -  第三步】如果没有找到上面的几种方法,则会同时查找countOf<Key>,enumeratorOf<Key>和 memberOf<Key>这三个方法

    -  如果这三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有 NSSet 消息转换为countOf<Key>,enumeratorOf<Key>和 memberOf<Key>:消息的某种组合,用于创建它的对象

    - 如果还是没有找到,则进入【第四步】

    - 【第四步】如果还没有找到,检查类方法InstanceVariablesDirectly是否YES,依次搜索_<key>,_is<Key>,<key>或 is<Key>的实例变量

    - 如果搜到,直接获取实例变量的值,进入【第五步】

    - 【第五步】根据搜索到的属性值的类型,返回不同的结果

    - 如果是对象指针,则直接返回结果

    - 如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它

    - 如果是是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象

    - 【第六步】如果上面5步的方法均失败,系统会执行该对象的valueForUndefinedKey:方法,默认抛出 NSUndefinedKeyException 类型的异常

    #### 综上所述:

    KVC通过 valueForKey: 方法取值的流程以设置LGPerson的对象person 的属性 name 为例,如下图所示

    ### 自定义KVC

    原理:通过给NSObject添加分类RCKVC,实现自定义的rc_setValue:forKey:和rc_valueForKey:方法,根据苹果官方文档提供的查找规则进行实现

    ~~~

    @interfaceNSObject(RCKVC)

    //设值

    -(void)rc_setValue:(nullable id)value forKey:(NSString*)key;

    //取值

    -(nullable id)rc_valueForKey:(NSString*)key;@end

    ~~~

    自定义 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)cj_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;

    // 拼接方法

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

    // _ _is is

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

    }

    ~~~

    ### 自定义 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)cj_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 @"";

    }

    ~~~

    使用路由访问,即 keyPath

    在日常开发中,一个类的成员变量有可能是自定义类或者其他的复杂数据类型,一

    般的操作是,我们可以先通过 KVC 获取该属性,然后再通过 KVC 获取自定义类的属

    性,就是比较麻烦,还有另一种比较简便的方法,就是使用 KeyPath 即路由,涉及

    以下两个方法:setValue:forKeyPath: 和 valueForKeyPath:

    ~~~

    //通过KeyPath来取值

    -(nullable id)valueForKeyPath:(NSString*)keyPath;

    //通过KeyPath来设值

    -(void)setValue:(nullable id)value forKeyPath:(NSString*)keyPath;

    ~~~

    参考如下的案例

    ~~~

    //RCPerson类

    @interfaceRCPerson:NSObject

    @property(nonatomic,copy)NSString*age;

    @property(nonatomic,strong)RCStudent*student;

    @end

    //RCStudent类

    @interface RCStudent:NSObject

    @property(nonatomic,copy)NSString*name;

    @end

    int main(intargc,constchar*argv[]){

    @autoreleasepool{

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

    RCStudent*student=[RCStudent alloc];student.name=@"RC";person.student=student;

    //根据kvc - keyPath路由修改student的subject属性的值

    [person setValue:@"嘻嘻"forKeyPath:@"student.name"];

    NSLog(@"%@",[person valueForKeyPath:@"student.name"]);

    }

    return0;

    }

    //*************打印结果*************2020-10-2709:55:08.512833+0800001-KVC简介[58089:6301894]改变前:CJL2020-10-2709:55:08.512929+0800001-KVC简介[58089:6301894]改变后:嘻嘻

    ~~~

    KVC 使用场景1、动态设值和取值

    - 常用的可以通过setValue:forKey:和valueForKey:

    - 也可以通过路由的方式setValue:forKeyPath:和valueForKeyPath:

    2、通过 KVC 访问和修改私有变量在日常开发中,对于类的私有属性,在外部定义的对象,是无法直接访问私有属性

    的,但是对于 KVC 而言,一个对象没有自己的隐私,所以可以通过 KVC 修改和访问

    任何私有属性

    3、多值操作(model 和字典互转)

    model 和字典的转换可以通过下面两个 KVC 的 API 实现

    【代码】

    ~~~

    // 字典转模型

    - (void)setValuesForKeysWithDictionary:(NSDictionary*)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 RCCustom[60035:6380757] English

    2020-10-27 11:33:43.377773+0800 RCCustom[60035:6380757] Franch

    2020-10-27 11:33:43.377860+0800 RCCustom[60035:6380757] Chinese

    2020-10-27 11:33:43.378233+0800 RCCustom[60035:6380757] 7

    2020-10-27 11:33:43.378327+0800 RCCustom[60035:6380757] 6

    2020-10-27 11:33:43.378417+0800 RCCustom[60035:6380757] 7

    ~~~

    相关文章

      网友评论

          本文标题:KVC底层原理

          本文链接:https://www.haomeiwen.com/subject/pjfzmrtx.html