iOS新手学习

iOS底层--KVC实现原理

2020-06-09  本文已影响0人  Engandend

KVC 是 Key-Value Coding的简称。是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议提供对其属性的间接访问。

请注意:这里说的很明确,是访问属性的

KVC 缺点:
1、KVC是使用过程中 ,是直接使用字符串进行操作,编译器不会进行检查,不易发现错误。后期维护也可能出现问题。
2、有时候取值可能也不是我们想要的类型。(可以拉到最后看注意点里面的 1、2)。

结论(具体步骤)

KVC虽然是NSObject的方法(来自 NSObject的分类@interface NSObject(NSKeyValueCoding))。但是其实现在Foundation.framework里,这里面是看不到源码的实现。
我们借助官方文档的介绍,先看结论

set(赋值)
1、找set<key>_set<Key>setIsName(setIsName文档上没有,但是确实是调用了)的方法,如果有,调用找到的方法 并结束。
2、如果没找到,就去找accessInstanceVariablesDirectly这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常(步骤4)。
3、在2满足的情况下 ,按照这个顺序_<key>_is<Key><key>is<Key>找成员变量,并将value赋值给这个成员变量。
4、如果上面的步骤都失败了,调用setValue:forUndefinedKey:抛出异常。

get(取值) Object对象的流程
1、按顺序找get<Key><key>is<Key>_<key>的方法,如果有,调用找到的方法 并结束。
2、尝试获取一个NSArray:
2.1 、如果找到countOf<Key>objectIn<Key>AtIndex:这2个方法,会根据这2个方法创建一个数组并返回
2.2、如果找到countOf<Key><key>AtIndexes:这2个方法,也同样会返回一个数组
3、尝试获取一个NSSet:
同时找到3个方法:countOf<Key>enumeratorOf<Key>memberOf<Key>:,如果找到,会返回一个NSSet
4、如果没找到,就去找accessInstanceVariablesDirectly这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常
4、按照这个顺序_<key>_is<Key><key>is<Key>找成员变量,并取出该成员变量的值。
5、进行细节处理:
5.1、如果是object 指针,直接返回结果
5.2、如果是NSNumber支持的标量类型,存储NSNumber 并返回
5.3、如果结果是NSNumber不支持的标量类型,转换成NSValue对象并返回
6、如果上面的步骤都失败了,调用valueForUndefinedKey:抛出异常。

详细流程如下图: KVC设值、取值流程

验证步骤

官方文档有很详细的讲解,这里做记录和自己的补充理解。(官方文档是英文版,如果引文不太好的同学,可以借助Chrome自带的翻译功能,准确率。。。。。至少比看不懂英文要强一点。。。)。

set(赋值)

先看以下代码:

//定义一个NSObject类  Person
//添加一个公开的成员变量(@public  方便打印)
//添加一个NSString属性
//添加一个NSInteger属性
@interface Person : NSObject
{
    @public
    NSString    *jeName;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age; 
@end

// 对其内容进行赋值
Person *p = [Person new];
[p setValue:@"nameValue" forKey:@"name"];
//[p setValue:@"nameValue" forKey:@"_name"];
[p setValue:@"jeNameValue" forKey:@"jeName"];
[p setValue:@4 forKey:@"age"];
NSLog(@"name = %@,  jeName = %@  age = %@",p.name,p->jeName,@(p.age));

// 查看打印信息
name = nameValue,  jeName = jeNameValue  age = 4

从上面的代码中,有几个疑问:

为了搞清问题,先了解KVC原理。

在文档的Search Pattern for the Basic Setter部分中看到这样的介绍。

1、找set<key>_set<Key>的方法,如果有,调用找到的方法 并结束。
2、如果没找到,就去找accessInstanceVariablesDirectly这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常(步骤4)。
3、在2满足的情况下 ,按照这个顺序_<key>_is<Key><key>is<Key>找成员变量,并将value赋值给这个成员变量。
4、如果上面的步骤都失败了,调用setValue:forUndefinedKey:抛出异常。

accessInstanceVariablesDirectly:
方法含义解释:--- 是否直接访问成员变量---- 如果有类似的成员变量,可不可以让我给它赋值

什么意思呢? 用代码来看具体是怎样调用的。

//定义一个NSObject类 Person   implementation 中不做任何操作
// 添加一个Person的分类 Person+JE
// 在分类中添加一个 jeName属性 (在person中添加属性,会被编译器自动生成set、get方法,不便与研究set、get流程)

@interface Person (JE)
@property (nonatomic, strong) NSString *jeName; 
@end

@implementation
#pragma mark - 关闭或开启实例变量赋值 (不实现的情况下,默认返回YES)
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//set方法1
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
//set方法2
- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
//set方法3  ---------  这个方法官方文档上面没有提,但是确实是调用了
- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
@end


//对jeName 用KVC进行赋值
Person *p = [Person new];
[p setValue:@"jeNameValue" forKey:@"jeName"];
NSLog(@"jeName = %@",p->jeName);

步骤1的验证 做以下操作

操作 3个set方法都保留 屏蔽set1 屏蔽set1、2 3个set都屏蔽
打印方法 打印方法1 打印方法2 打印方法3 -
jeName 值 nil nil nil jeNameValue
jeName 值的原因 对set进行覆盖,但是set内未赋值 同左 同左 未实现set方法,进行了步骤3

这样可以验证系统是按照set<key>_set<key>setIs<key>的步骤去走set方法

// 只有Person类,不创建分类
@interface Person : NSObject
{
    @public //方便外部打印
    NSString    *_jeName;      //成员变量1
    NSString    *_isJeName;    //成员变量2
    NSString    *jeName;      //成员变量3
    NSString    *isJeName;    //成员变量4
}
@end

.m 文件不做任何操作

//外部KVC赋值 并打印
Person *p = [Person new];
    [p setValue:@"jeNameValue" forKey:@"jeName"];
    //[p setValue:@4 forKey:@"age"];
    NSLog(@"_jeName = %@  _isJeName = %@  jeName = %@  isJeName = %@",p->_jeName,p->_isJeName,p->jeName,p->isJeName);

打印结果
_jeName = jeNameValue  _isJeName = (null)  jeName = (null)  isJeName = (null)

步骤3操作

操作 4个成员变量都存在 屏蔽成员变量1 屏蔽成员变量1、2 屏蔽成员变量1、2、3 屏蔽成员变量1、2、3 、4
分别打印成员变量值 变量1有值 变量2 有值 变量3 有值 变量4 有值 抛出异常

走到这里就知道为何可以对成员变量进行赋值了。其实是因为 系统在查找3个set方法没找到情况下,走了步骤3,对类似的成员变量进行了赋值

get(取值)

还是先看文档在文档的
Search Pattern for the Basic Getter部分中看到这样的介绍。

1、按顺序找get<Key><key>is<Key>_<key>的方法,如果有,调用找到的方法 并结束。
2、如果没找到,就去找accessInstanceVariablesDirectly这个方法,如果这个方法返回YES,就可以进行下一步,如果返回NO,就抛出异常4。
3、在2满足的情况下 ,按照这个顺序_<key>_is<Key><key>is<Key>找成员变量,并取出该成员变量的值。
4、如果上面的步骤都失败了,调用valueForUndefinedKey:抛出异常。

用下面代码验证

//  创建一个Person类并什么代码都不写
// 创建一个Person的分类  Person+JE
// 在Person+JE.m 中添加以下4个方法
@implementation Person (JE)
//方法1
- (NSString *)getJeName {
    NSLog(@"%s ",__func__);
    return @"";
}
//方法2
- (NSString *)jeName {
    NSLog(@"%s ",__func__);
    return @"";
}
//方法3
- (NSString *)isJeName {
    NSLog(@"%s ",__func__);
    return @"";
}
//方法4
- (NSString *)_jeName {
    NSLog(@"%s ",__func__);
    return @"";
}
@end

都不屏蔽 //打印方法1
屏蔽方法1 // 打印方法2
方法1、2 // 打印方法3
方法1、2、3 // 打印方法4
方法1、2、3、4 // 抛出异常 valueForUndefinedKey

@interface Person : NSObject
{
    @public //方便外部打印
    NSString    *_jeName;      //成员变量1
    NSString    *_isJeName;    //成员变量2
    NSString    *jeName;      //成员变量3
    NSString    *isJeName;    //成员变量4
}
@end

外部对4个变量赋值 并通过KVC对jeName 进行取值
Person *p = [Person new];
p->_jeName = @"_jeName";
p->_isJeName = @"_isJeName";
p->jeName = @"isJeName";
p->isJeName = @"isJeName";

NSString *kvcValue = [p valueForKey:@"jeName"];

NSLog(@"jeName = %@",kvcValue);

可以分别屏蔽 成员变量1、2、3、4 查看打印结果,来验证步骤3。

KVC补充

1、keyPath

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
是KVC 一级/多级 赋值/取值的一种方式。
比如以下代码:Person里面有一个 model 属性

// Person : NSObject     
//TestModel : NSObject
@interface Person : NSObject
@property (nonatomic, strong) TestModel *model;
@end

@interface TestModel : NSObject
@property (nonatomic, strong) NSString *modelName;
@end

可以直接通过keyPath的方式对model属性里面的 modelName进行赋值

Person *p = [Person new];
[p setValue:[TestModel new] forKey:@"model"];  
// 等同   p.model = [TestModel new];
// 等同 [p setValue:[TestModel new] forKeyPath:@"model"];  

[p setValue:@"modelName" forKeyPath:@"model.modelName"];

NSLog(@"modelName = %@",p.model.modelName);

keyPath 注意点:
1、比必须保证赋值的上一级是有值的(内存不为空)否则赋值无效。
2、可以对一级、多级赋值(上面代码中,初始化model属性的时候,forKey: 或者 forKeyPath:都是可行的)
3、forKey 只能对一级进行赋值(比如上面代码中model中的modelName进行初始化的时候,用forKey的方式的话 会报错)

// 上面代码中的 TestModel

NSMutableArray *tempArr = [NSMutableArray new];
    for (NSInteger i = 0; i < 3; i ++) {
        TestModel *model = [TestModel new];
        [tempArr addObject:model];
    }
//    NSArray *arr = [tempArr valueForKey:@"modelName"];  和下面的 valueForKeyPath 效果相同
    NSArray *arr = [tempArr valueForKeyPath:@"modelName"];

//Arr 的打印结果
<__NSArrayI 0x6000020d0cf0>(
<null>,
<null>,
<null>
)

2、集合操作符

集合操作符是在调用[valueForKeyPath:]根据keypath中的特定符号,返回数据之前以某种方式对数据操作,官方文档戳这里

集合操作符的标准写法:
[ obj valueForKeyPath:@"leftKeypath.@collectionOperator.rightKeyPath"]

官方图
当操作对象本身是NSArray类型的时候,leftKeyPath可以省略

举例:

//创建一个Model,有一个NSArray类型的属性
@interface TestModel : NSObject
@property (nonatomic, strong) NSMutableArray<TestChildModel *> *childArr;
@end

@interface TestChildModel : NSObject
@property (nonatomic, assign) NSInteger childAge; 
@end

// 创建一个TestModel 并对其进行内容赋值 
//下面2种写法都是一样的
id obj1 = [model valueForKeyPath:@"childArr.@sum.childAge"];  //操作对象是NSObject,那么leftKeyPath 就不能省略
id obj2 = [model.childArr valueForKeyPath:@"@sum.childAge"];  //操作对象是model.childArr 数组类型,那么leftKeyPath 就能省略,而且必须要省略

集合操作符包括:

KVC 细节(注意点)

//创建一个Person类 并添加一个公开成员变量
@interface Person : NSObject {
    @public //方便外部赋值、取值
    NSString    *_age;
}

//创建一个Person分类 Person+JE 并添加一个属性age
@interface Person (JE)
@property (nonatomic, assign) NSInteger age; 
@end

//在外部进行赋值并打印valueForKey:
Person *p = [Person new];
p->_age = @"1";
id age = [p valueForKey:@"age"];
NSLog(@"age = %@",age);

打印结果是 age = "1"; age是NSString类型。
因为是直接取到_age的值并返回,所以,即使定义的是NSInteger 类型,最后运行时,得到的依然是NSString

Person *p = [Person new];
[p setValue:@12 forKey:@"age"];
id age = [p valueForKey:@"age"];
NSString *_age = p->_age;
NSLog(@"age = %@  cls = %@ \
      _age = %@ cls = %@",age,[age class],_age,[_age class]);

可以先猜想打印结果。
其实这里的age 和 _age 最终的取值 都是来自Person中的公开成员变量 NSString *_age
age = 12 cls = __NSCFNumber _age = 12 cls = __NSCFNumber

@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) NSString *name;
@end

//外部进行赋值操作
Person *p = [Person new];
//    [p setValue:@"a" forKey:@"age"];
[p setValue:@"12" forKey:@"age"];
[p setValue:@(13) forKey:@"name"];

猜测能否正常运行,如果正常运行,得到的age值是什么类型?
能正常运行
1、最后得到的age 还是 NSInteger(本身定一个属性的类型),值为12
2、最后的name 是 NSnumber 类型的13
因为在get取值步骤中,只说道,对NSnunber类型的属性有自动转换类型的功能,但是并没有对其他类型的属性自动转换,所有age被自动转换为NSInteger,而name由于运行时机制,最后得到的是NSInteger

如果赋值为@"a",结果得到的age = 0; 因为@"a"转成integer类型就是0
也就是说。KVC赋值 具有自动转换类型的功能。符合get流程中的5.2

系统对NSMutableDictionary的 setValue: forKey:NSDictionary 的valueForKey: 进行了重写

@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)
/* Return the result of sending -objectForKey: to the receiver.
*/
- (nullable ObjectType)valueForKey:(NSString *)key;
@end

@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)
/* Send -setObject:forKey: to the receiver, unless the value is nil, in which case send -removeObjectForKey:.
如果赋值为nil, 将会调用 removeObjectForKey 方法
*/
- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;
@end

所以 :在发送网络请求的时候,组装一个NSMutableDictionary 时,会有这样的写法:[dic setValue:@"value" forKey:@"key"]; 来达到为dictionary添加一个key的目的。

上一篇 下一篇

猜你喜欢

热点阅读