iOS 底层探索:KVC 底层原理

iOS 底层探索: 学习大纲 OC篇

前言

  • KVC用的好多,今天来看看他的底层是怎么实现的。这里底层流程看起来有些粗糙,了解就行;
  • KVC机制是在Foundation框架中,而Foundation框架是不开源的,可以看
    官方文档: Key-Value Coding Programming Guide

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.

内容

  • KVC定义及相关API
  • KVC 设值 底层原理
  • KVC 取值 底层原理
  • KVC 使用场景
  • 拓展: 异常处理、 属性验证、 KVC处理非对象

一、 KVC定义及相关API

KVC的全称是Key-Value Coding,翻译成中文是 键值编码,键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议来间接访问其属性。既可以通过一个字符串key来访问某个属性。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。

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

//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;

//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;

//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; 

//通过KeyPath来设值                 
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

其他API:

//默认返回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 设值 底层原理

针对赋值,Key-Value Coding Programming Guide中有这样一段

Search Pattern for the Basic Setter
The default implementation of setValue:forKey:, given key and value parameters as input, attempts to set a property named key to value (or, for non-object properties, the unwrapped version of value, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:

Look for the first accessor named set<Key>: or _set<Key>, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.

If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.

Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

对于setValue:forKey:给定的key和value参数作为输入,尝试设置命名属性key到value主要流程为

  1. 按顺序查找名为set<Key>或 _set<Key>方法,如果找到,直接调用。如果没找到,则第2步
  2. 调用类方法accessInstanceVariablesDirectly 如果返回Yes,则进入间接访问实例化赋值,寻找名为_<key>,_is<Key>,<key>,或者is<Key>的变量,如果找到,则直接使用输入值设置变量。
  3. 如果返回NO或者没找到上面的值,则调用setValue:forUndefinedKey:,默认会抛出异常。

举个例子:

LGPerson *person = [[LGPerson alloc] init];
// 1、一般setter 方法
person.name    = @"AA";
// 2、KVC方式
[person setValue:@"CC" forKey:@"name"]; 

流程大概如下图:


kvc 赋值流程图.png

二、KVC 取值 底层原理

针对赋值,Key-Value Coding Programming Guide中有这样一段

 
Accessor Search Patterns
The default implementation of the NSKeyValueCoding protocol provided by NSObject maps key-based accessor calls to an object’s underlying properties using a clearly defined set of rules. These protocol methods use a key parameter to search their own object instance for accessors, instance variables, and related methods that follow certain naming conventions. Although you rarely modify this default search, it can be helpful to understand how it works, both for tracing the behavior of key-value coded objects, and for making your own objects compliant.

NOTE

The descriptions in this section use <key> or <Key> as a placeholder for the key string that appears as a parameter in one of the key-value coding protocol methods, which is then used by that method as part of a secondary method call or variable name lookup. The mapped property name obeys the placeholder’s case. For example, for the getters <key> and is<Key>, the property named hidden maps to hidden and isHidden.

Search Pattern for the Basic Getter
The default implementation of valueForKey:, given a key parameter as input, carries out the following procedure, operating from within the class instance receiving the valueForKey: call.

Search the instance for the first accessor method found with a name like get<Key>, <key>, is<Key>, or _<key>, in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.

If no simple accessor method is found, search the instance for methods whose names match the patterns countOf<Key> and objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSArray class) and <key>AtIndexes: (corresponding to the NSArray method objectsAtIndexes:).

If the first of these and at least one of the other two is found, create a collection proxy object that responds to all NSArray methods and return that. Otherwise, proceed to step 3.

The proxy object subsequently converts any NSArray messages it receives to some combination of countOf<Key>, objectIn<Key>AtIndex:, and <key>AtIndexes: messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name like get<Key>:range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSArray, even if it is not.

If no simple accessor method or group of array access methods is found, look for a triple of methods named countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class).

If all three methods are found, create a collection proxy object that responds to all NSSet methods and return that. Otherwise, proceed to step 4.

This proxy object subsequently converts any NSSet message it receives into some combination of countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSSet, even if it is not.

If no simple accessor method or group of collection access methods is found, and if the receiver's class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.

If the retrieved property value is an object pointer, simply return the result.

If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that.

If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.

If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

对于取值,文档中有以上说明,其实类似于上面的赋值原理。

在valueForKey:给定key参数作为输入的情况下

  1. 首先查找getter方法,按照get<Key> -> <key> -> is<Key> -> _<key>的方法顺序查找 如果找到则进入步骤5,如果没找到,则下一步
  2. 如果步骤1中的getter方法没有找到,KVC会查找countOf <Key>和objectIn <Key> AtIndex :和<key> AtIndexes : 如果找到其中的第一个以及其他两个中的至少一个,则创建一个响应所有NSArray方法的集合代理对象,并返回该对象。否则,请继续执行步骤3。代理对象随后将任何NSArray接收到的一些组合的消息countOf<Key>,objectIn<Key>AtIndex:和<key>AtIndexes:消息给KVC创建它兼容的对象。如果原始对象还实现了名称为的可选方法get<Key>:range:,则代理对象也会在适当时使用该方法。
  3. 如果没有找到简单的访问方法或阵列访问方法组,寻找一个三重的方法命名countOf<Key>,enumeratorOf<Key>和memberOf<Key>:(对应于由所定义的原始的方法NSSet类)。如果找到所有三个方法,请创建一个响应所有NSSet方法的集合代理对象并将其返回。否则,请继续执行步骤4。
  4. 如果此时还未找到,且接收器的类方法accessInstanceVariablesDirectly返回YES,搜索名为实例变量_<key>,_is<Key>,<key>,或者is<Key>,按照这个顺序。如果找到,请直接获取实例变量的值,然后继续执行步骤5。否则,请继续执行步骤6。
  5. 如果检索到的属性值是对象指针,则只需返回结果。
    如果该值是所支持的标量类型NSNumber,则将其存储在NSNumber实例中并返回该实例。
    如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。
  6. 如果其他所有方法均失败,请调用valueForUndefinedKey:。默认情况下会引发异常

流程大概如下图:


三、KVC 使用场景

估计都会使用就不举例了

  • 利用KVC动态的取值和设值

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

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

  • Model和字典转换

  • 用KVC来访问和修改私有变量

    根据上面的实现原理我们知道,KVC本质上是操作方法列表以及在内存中查找实例变量。我们可以利用这个特性访问类的私有变量,例如下面在.m中定义的私有成员变量和属性,都可以通过KVC的方式访问。【对于KVC而言,一个对象没有自己的隐私,所以可以通过KVC修改和访问任何私有属性

    这个操作对readonly的属性,@protected的成员变量,都可以正常访问。如果不想让外界访问类的成员变量,则可以将accessInstanceVariablesDirectly属性赋值为NO

  • 修改一些控件的内部属性

    这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些控件的API(常用的就是自定义tabbar、个性化UITextField中的placeHolderText),这样我们就无法正常地访问和修改这些控件的样式。而KVC在大多数情况可下可以解决这个问题。

  • 用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

四、拓展

KVC属性验证

KVC提供了属性值,用来验证key对应的Value是否可用的方法

  • 在调用KVC时可以先进行验证,验证通过下面两个方法进行,支持keykeyPath两种方式。验证方法默认实现返回YES,可以通过重写对应的方法修改验证逻辑。

验证方法需要我们手动调用,并不会在进行KVC的过程中自动调用。

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;

这个方法的默认实现是去探索类里面是否有一个这样的方法:-(BOOL)validate<Key>:error:如果有这个方法,就调用这个方法来返回,没有的话就直接返回YES

@implementation Address
-(BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{  //在implementation里面加这个方法,它会验证是否设了非法的value
    NSString* country = *value;
    country = country.capitalizedString;
    if ([country isEqualToString:@"Japan"]) {
        return NO;                                                                             //如果国家是日本,就返回NO,这里省略了错误提示,
    }
    return YES;
}
@end

NSError* error;
id value = @"japan";
NSString* key = @"country";
BOOL result = [add validateValue:&value forKey:key error:&error]; //如果没有重写-(BOOL)-validate<Key>:error:,默认返回Yes
if (result) {
    NSLog(@"键值匹配");
    [add setValue:value forKey:key];
}
else{
    NSLog(@"键值不匹配"); //不能设为日本,其他国家都行
}
NSString* country = [add valueForKey:@"country"];
NSLog(@"country:%@",country);

//打印结果 
KVCDemo[867:58871] 键值不匹配
KVCDemo[867:58871] country:China

KVC处理非对象

KVC是支持基础数据类型和结构体的,可以在setter和getter的时候,通过NSValue和NSNumber来转换为OC对象。该方法valueForKey:总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber或者NSValue对象。这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开发者需要手动转换成原来的类型。尽管valueForKey:会自动将值类型封装成对象,但是setValue:forKey:却不行。你必须手动将值类型转换成NSNumber或者NSValue类型,才能传递过去。

  • 可以调用initWithBool:方法对基础数据类型进行包装
@property (nonatomic, assign, readonly) BOOL boolValue;
- (NSNumber *)initWithBool:(BOOL)value 

KVC异常处理

    1. key或者keyPath发生错误

当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个NSUndefinedKeyException的异常,并且应用程序Crash
我们可以重写下面两个方法:

//获取了不存在的key 只需要实现如下方法
- (nullable id)valueForUndefinedKey:(NSString *)key;
//设置了不存在的key 造成崩溃 只需要实现如下方法
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
    1. 传参为nil

通常情况下,KVC不允许你要在调用setValue:属性值 forKey:(或者keyPath)时对非对象传递一个nil的值。因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。

- (void)setNilValueForKey:(NSString *)key{
   NSLog(@"-----设置了nil");
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容