iOS 底层探索之KVC

参考官方文档

KVC概述

键值编码是一种由NSKeyValueCoding非正式协议启用的机制,对象采用该机制提供对其属性的间接访问。键值编码是一个基本概念,是许多其他Cocoa技术的基础,在某些情况下,键值编码还有助于简化代码。

KVC可以通过键值的方式对对象的属性进行存取操作,这使得在运行时操作对象的属性成为可能。

我们通常都是使用gettersetter方法去访问和设置属性的值,有了KVC我们可以直接访问属性的基础实例变量,当然这样可读性和性能并不是很高,一般不推荐这么去用。

KVC的使用大全

Part1、普通用法
1、取值valueForKey:
普通对象

[stu valueForKey:@"age"];

数组等集合对象的处理

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

2、赋值setValue:forKey:

[stu setValue:@(18) forKey:@"age"];

3、批量处理
给定字典可以根据keys的数组返回所有符合的键值对,以字典的形式返回
dictionaryWithValuesForKeyssetValuesForKeysWithDictionary.

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int length;
@property (nonatomic, strong) NSMutableArray *penArr;
@property (nonatomic, strong) Student *stu;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [Person new];
    NSDictionary* dict = @{
                           @"name":@"Tom",
                           @"age":@(18+i),
                           @"nick":@"Cat",
                           @"length":@(180 + 2*arc4random_uniform(6)),
                          };
    [p setValuesForKeysWithDictionary:dict];
}

@end

Part2、keyPath用法
1、将数组中所有对象的name属性值取出,并放入一个数组中返回

NSArray *names = [array valueForKeyPath:@"name"];

同理,也可以获取每个数组元素的length属性

- (void)arrayMessagePass{
    NSArray *array = @[@"First",@"Second",@"Third",@"Forth"];
    NSArray *lenStr= [array valueForKeyPath:@"length"];
    NSLog(@"lenStr = %@",lenStr);
    NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
    NSLog(@"lowStr = %@",lowStr);
}

打印如下


image.png

2、在keyPath里,key也可以使用点语法。

@interface Student : NSObject
{
    @public
    NSArray *books;
    NSString *name;
    int age;
}

@property (nonatomic, copy) NSString *sex;

@end

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int length;
@property (nonatomic, strong) NSMutableArray *penArr;
@property (nonatomic, strong) Student *stu;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p = [Person new];
    NSLog(@"%@", [p valueForKeyPath:@"stu.sex"]);
}

@end
容错处理

当KVC经过搜索模式没有获取到对应的值时,会调用相对应的异常方法,导致应用程序Crash,常见的异常有三种,为了防止出现这种严重的错误,我们通常的做法是重写相对应的异常方法,如下所示
1、'NSUnknownKeyException', reason: '[<Person 0x6000016f1e00> valueForUndefinedKey:]: this class is not key value coding-compliant for the key hahaName.'

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"%@的值不能为空",key);
}

2、'NSUnknownKeyException', reason: '[<Person 0x6000008ffe80> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xixiName.'

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"不能对不存在的健赋值");
}

3、NSInvalidArgumentException', reason: '[<Person 0x60000295ca00> setNilValueForKey]:

- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"不能对不存在的键取值");
    return @"error";
}
KVC集合类运算

使用 KVC 进行集合类的运算,例如求一个数组中所有Person对象的length总和。


image.png

如图所示,

  • keyPathToCollection:Left key path,要操作的集合对象,若调用 valueForKeyPath: 方法的对象本来就是集合对象,则可以省略;
  • collentionOperator:Collection operator,集合操作符,一般以@开头;
  • keyPathToproperty:Right key path,要运算的属性。

集合运算符分为三种:集合操作符(返回NSNumber)、数组操作符(返回数组)、嵌套操作符
Part1、集合操作符(Aggregation Operators)
返回的是NSNumber类型的值,有以下5种用法,分别是平均、总和、数量、最大、最小。

  • 1、@avg用来计算指定属性的平均值,返回NSNumber
  • 2、@sum属性总和,返回NSNumber
  • 3、@count用来计算集合里对象的数量
  • 4、@max属性最大值
  • 5、@min属性最小值

用法如下所示

NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        Person *p = [Person new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(180 + 2*arc4random_uniform(6)),
                               };
        [p setValuesForKeysWithDictionary:dict];
        [personArray addObject:p];
    }
    NSLog(@"%@", [personArray valueForKey:@"length"]);
    
    float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
    NSLog(@"集合中有length属性的元素,length属性的平均值为 = %f", avg);
    int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
    NSLog(@"集合中有length属性的元素的数量为 = %d", count);
    int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
    NSLog(@"集合中有length属性的元素,length属性的总和为 = %d", sum);
    int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
    NSLog(@"集合中有length属性的元素,length属性的最大值为 = %d", max);
    int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
    NSLog(@"集合中有length属性的元素,length属性的最小值为 = %d", min);

打印结果如下


image.png

max和min是通过compare:方法进行比对的,所以需要属性满足能通过compare方法比对

Part2、数组操作符(Array Operators)
与集合操作符的区别是,数组操作符返回值的是数组。它有以下2种用法

  • 1、@unionOfObjects返回指定属性的数组
  • 2、@distinctUnionOfObjects返回指定属性去重后的集合,即数组中payee属性值重复的都去掉(去重)。
NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        Person *p = [Person new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [p setValuesForKeysWithDictionary:dict];
        [personArray addObject:p];
    }
    NSLog(@"%@", [personArray valueForKey:@"length"]);
    // 返回操作对象指定属性的集合
    NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.length"];
    NSLog(@"arr1 = %@", arr1);
    // 返回操作对象指定属性的集合 -- 去重
    NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.length"];
    NSLog(@"arr2 = %@", arr2);

打印结果如下:

image.png

从结果中我们知道,@unionOfObjects的作用是返回数组元素中符合length属性的元素;而@distinctUnionOfObjects在此基础上做了一步去重操作。
Part3、嵌套数组
嵌套运算符在嵌套集合上运行,集合的每个元素本身都包含一个集合。它有以下2种用法

  • 1、@unionOfArrays:valueForKeyPath创建并返回一个数组,该数组包含和属性对应的集合的所有对象,且不删除重复项
  • 2、@distinctUnionOfArrays:valueForKeyPath创建并返回一个数组,该数组包含和属性对应的所有集合的组合的不同对象,就是去重。
NSMutableArray *personArray1 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGPerson *person = [LGPerson new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personArray1 addObject:person];
    }
    
    NSMutableArray *personArray2 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGPerson *person = [LGPerson new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personArray2 addObject:person];
    }
    
    // 嵌套数组
    NSArray* nestArr = @[personArray1, personArray2];
    
    NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];
    NSLog(@"unionOfArrays = %@", arr1);
    
    NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];
    NSLog(@"distinctUnionOfArrays = %@", arr);

打印结果如下:


image.png

从结果中我们可以得知,@unionOfArrays的作用是将所有嵌套的集合,通过属性全部整合到一个集合中了;而@distinctUnionOfArrays在此基础上又做了一步去重操作。

KVC的搜索模式

Part1、基于Getter的搜索模式

  • 1、先按顺序查找访问方法get<Key>,<key>,is<Key>,或者_<key>。
  • 2、如果getter方法没有找到,则尝试寻找countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndexes,如果找到了第一个和剩下两个中的一个方法,就会返回一个数组
  • 3、如果还是没有找到,则尝试countOf<Key>,enumeratorOf<Key>和memberOf<Key>:,如果上述三个方法都找到了,就去创建一个NSSet类型的对象,则会返回一个可以响应NSSet所有方法的对象。
  • 4、如果上述方式没有找到,判断accessInstanceVariablesDirectly的值为YES时,在内存中搜索_<key>,_is<Key>,<key>,或者is<Key>,找到了就执行5,没找到就执行步骤6.
  • 5、返回结果
  • 6、如果都失败了,调用valueForUndefinedKey:抛出异常。

举个例子看比较好理解一些

@interface Student : NSObject
{
    @public
    NSArray *books;
    NSString *name;
    int age;
}

@property (nonatomic, copy) NSString *sex;

@end

@implementation Student

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Student *stu = [Student new];
    stu->name = @"stupid bird";
    NSLog(@"name = %@", [stu valueForKey:@"name"]);
    //[stu setValue:@(18) forKey:@"age"];
}

@end

从代码中我们知道Student类包含了一个名为name的实例变量,它默认是没有实现settergetter方法的,在ViewDidLoad中给stuname属性赋值,再通过valueForKey:的方式取值,看看打印的结果是什么?

image.png

可以看到,获取到了name属性的结果,我们对比上面的Getter的搜索顺序,不难发现它走到了第4步,从内存中获取的name属性值。

这一次我们在Student类中增加两个方法

@implementation Student
//name****************
//返回name属性的个数
- (NSUInteger)countOfName {
    NSLog(@"%s", __func__);
    return 5;
}

//返回每个name属性对应的值
- (NSString *)objectInNameAtIndex:(NSUInteger)index {
    return [NSString stringWithFormat:@"name %lu", index];
}

@end

打印结果如下:


image.png

我们发现获取到的并不是name属性的结果,对比上面的Getter的搜索顺序,发现它是执行到了第2步,直接返回了一个属性为name的数组。

以上就是探索基于Getter的搜索模式

Part2、基于Setter的搜索模式

  • 1、按顺序查找名为set<Key>:_set<Key>方法,如果找到,则使用该方法进行赋值。
  • 2、如果没有找到,且accessInstanceVariablesDirectly返回YES,则在内存中查找_<key>_is<Key><key>,或者is<Key>的实例变量,按照这个顺序。如果找到,则直接赋值。
  • 3、如果还找不到,则调用setValue:forUndefinedKey:抛出异常。

以上是官方文档说的Setter赋值时的搜索模式,我们用代码实验一下

@interface Person : NSObject
@end

@implementation Person

#pragma mark - set相关
- (void)setName:(NSString *)name{
    NSLog(@"%s",__func__);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s",__func__);
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGPerson *p = [[LGPerson alloc] init];
    [p setValue:@"name" forKey:@"name"];
//    NSLog(@"%@",[p valueForKey:@"name"]);
}


@end

按照官方说的搜索步骤,先按顺序查找名为set<Key>:_set<Key>方法,看看打印结果

image.png

注释掉setName:方法试试

@implementation Person

//#pragma mark - set相关
//- (void)setName:(NSString *)name{
//    NSLog(@"%s",__func__);
//}

- (void)_setName:(NSString *)name{
    NSLog(@"%s",__func__);
}

@end

打印结果如下

image.png

好的,打印结果上看是从_setName方法中获取的,官方说的没错(噗)。

为了验证下面步骤,按如下修改Person类

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

@implementation Person
@end

viewDidLoad中打断点,如下所示

image.png

发现_name属性被赋值了,所以官方说的对
至此,就不再去质疑官方了,我记住了,在KVC进行Setter方法赋值时,若没有setter方法,会按照_<key>、_is<key>、<key>、is<key>的顺序赋值。(偷笑)

写这篇的目的是更好的理解KVC的机制,希望和大家一起进步。

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

推荐阅读更多精彩内容

  • 关于键值编码 键值编码(KVC)是一种由NSKeyValueCoding非正式协议提供的机制,对象采用该机制来提供...
    渐z阅读 859评论 0 0
  • 本文参考: KVC官方文档 KVC原理剖析 iOS KVC详解 KVC 简介 KVC全称是Key Value Co...
    拧发条鸟xds阅读 5,210评论 6 23
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32
  • 原文:iOS 关于KVC的一些总结 本文参考: KVC官方文档 KVC原理剖析 iOS KVC详解 KVC 简介 ...
    liyoucheng2014阅读 923评论 0 3
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    我的梦工厂阅读 880评论 1 8