iOS开发-KVC底层原理及实践探究

一、什么是KVC?

KVC的全称是Key-Value Coding,翻译成中文是 键值编码,键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议来间接访问其属性。实际上,KVC是对NSObject的协议扩展,当然也支持NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet。

1. 最常用的四个方法
//直接通过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;  
2. 最简单的使用例子

现在有一个银行账户类BankAccount,银行账户有余额currentBalance属性,还有所属的用户信息Person类。

BankAccount类

@class Person;
@class Transaction;

@interface BankAccount : NSObject
@property (nonatomic, strong) NSNumber          *currentBalance;  //当前余额
@property (nonatomic, strong) Person            *owner;  //账户

@end

Person类

@interface Person : NSObject
@property (nonatomic, copy) NSString            *name;          // 名字
@property (nonatomic, copy) NSString            *age;          // 年龄

@end

我们日常开发中使用最多的赋值就是下面这种

    BankAccount *bankAcount = [[BankAccount alloc]init];
    Person *person = [Person new];
    person.name = @"根哥";
    person.age = @"20";
    
    bankAcount.currentBalance = @2000;
    bankAcount.owner =  person;

接下来就是KVC赋值了

    BankAccount *bankAcount = [[BankAccount alloc]init];
    Person *person = [Person new];
    bankAcount.owner =  person; //此处必须先初始化一个实例对象,才可进行赋值。
    
    [bankAcount setValue:@(200) forKey:@"currentBalance"];
    [bankAcount setValue:@"28" forKeyPath:@"owner.age"];

这个简单的例子只是为了让你对KVC稍微有点了解。

二、KVC底层原理

那么KVC是怎么通过字符串Key就可以给对象赋值的了,这就要说到KVC的赋值逻辑了。

  • 当调用setValue:forKey:设置属性value时,其底层的执行流程为
  1. 首先去对象里面找是否有setter方法,如果有则给成员变量key赋值。
  2. 如果没有实现setter方法,则会看对象的
    + (BOOL)accessInstanceVariablesDirectly(它表示是否允许读取实例变量的值)是否返回YES,如果返回YES,则继续步骤3,否则会调用setValue:forUndefinedKey:方法,并抛出异常。
  3. 满足2的条件下 ,会查找一个命名规则为_<key>、_is<Key>、<key>、is<Key>的实例变量。根据这个顺序,如果发现则将value赋值给实例变量。
  • get的搜索规则
    get的搜索规则相对于set就有点复杂了,下面只做了解就可以了

1.通过getter方法搜索实例,例如get<Key>, <key>, is<Key>, <key>的拼接方案。按照这个顺序,如果发现符合的方法,就调用对应的方法并拿着结果跳转到第五步。否则,就继续到下一步。
2.如果没有找到简单的getter方法,则搜索其匹配模式的方法countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:。如果找到其中的第一个和其他两个中的一个,则创建一个集合代理对象NSKeyValueArray,该对象响应所有NSArray的方法并返回该对象。否则,继续到第三步。代理对象随后将NSArray接收到的countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:的消息给符合KVC规则的调用方。当代理对象和KVC调用方通过上面方法一起工作时,就会允许其行为类似于NSArray一样。
3.如果没有找到NSArray简单存取方法,或者NSArray存取方法组。则查找有没有countOf<Key>、enumeratorOf<Key>、memberOf<Key>:命名的方法。如果找到三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回。否则,继续执行第四步。此代理对象随后转换countOf<Key>、enumeratorOf<Key>、memberOf<Key>:方法调用到创建它的对象上。实际上,这个代理对象和NSSet一起工作,使得其表象上看起来是NSSet。
4.如果没有发现简单getter方法,或集合存取方法组,以及接收类方法accessInstanceVariablesDirectly是返回YES的。搜索一个名为
<key>、_is<Key>、<key>、is<Key>的实例,根据他们的顺序。如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步,否则,跳转到第六步。
5.如果取回的是一个对象指针,则直接返回这个结果。如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。
6.如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法。

三、应用场景

1,用KVC中的函数来操作集合(集合主要指NSArray和NSSet,不包括NSDictionary)

上面的图是集合运算符的格式,主要是对象调用valueForKeyPath:方法进行操作。运算符有三种:
1)简单集合运算符共有@avg, @count , @max , @min ,@sum5种

    Transaction *transaction = [[Transaction alloc]init];
    transaction.name = @"3";
    transaction.money = @23;

    Transaction *transaction1 = [[Transaction alloc]init];
    transaction1.name = @"3";
    transaction1.money = @1000;
    
    NSArray *tempArray = [NSArray arrayWithObjects:transaction,transaction1, nil];
    
    NSNumber *maxValue = [tempArray valueForKeyPath:@"@max.money"];
就是遍历数组的每一个对象,然后对应属性的最大值。

2)对象运算@distinctUnionOfObjects返回去重后的数组,@unionOfObjects返回数组

    Transaction *transaction = [[Transaction alloc]init];
    transaction.name = @"3";
    transaction.money = @23;

    Transaction *transaction1 = [[Transaction alloc]init];
    transaction1.name = @"3";
    transaction1.money = @1000;
    
    NSArray *tempArray = [NSArray arrayWithObjects:transaction,transaction1, nil];
    
    NSNumber *avg = [tempArray valueForKeyPath:@"@max.money"];
    
    
    //返回对象中name的数组 下面两种方式一样
    NSArray *tempArray1 = [tempArray valueForKeyPath:@"@unionOfObjects.name"];  //[@"3", @"3"]
    NSArray *resultArray = [tempArray valueForKeyPath:@"name"];  //[@"3", @"3"]
    //    NSSet *set = [NSSet setWithArray:tempArray1]; //去重

    //@distinctUnionOfObjects将集合对象中,所有Transaction对象放在一个数组中,并将数组进行去重后返回。  比unionOfObjects多一步,会去重
    NSArray *tempArray2 = [tempArray valueForKeyPath:@"@distinctUnionOfObjects.name"]; //[@"3"]

3)Array和Set嵌套操作符@distinctUnionOfArrays,@distinctUnionOfSets,@unionOfArrays
就是数组或集合的双重嵌套。

    NSArray *dArray = @[tempArray,tempArray];
    NSArray *resultArray2 = [dArray valueForKeyPath:@"@distinctUnionOfArrays.name"]; //[@"3"]
2. 使用KVC实现高阶消息传递(就是对集合中的所有对象都执行某个方法,跟上面的函数运算类似)
  NSArray* stringArray = @[@"china",@"japan",@"korea"];
    NSArray* capArray = [stringArray valueForKey:@"capitalizedString"]; //执行首字符转大写的操作
    for (NSString* string  in capArray) {
        NSLog(@"首字母大写:%@",string);  //China Japan Korea
    }
    NSArray* arrCapStrLength = [stringArray valueForKeyPath:@"capitalizedString.length"];
    for (NSNumber* length  in arrCapStrLength) {
        NSLog(@"%ld",(long)length.integerValue);  5 5 5
    }

此处有个非常实用的方法,就是将字符串的首字符转为大写其余小写#capitalizedString#

NSString *name = @"name";
name = name.capitalizedString;  //Name

NSString *testString = @"helloWord";
testString = testString.capitalizedString;//Helloword

3, 多值操作(model和字典互转)
  • 假设dict字典中有name,icon 的Key,XGYModel模型类中必须要有同名的name,icon属性与之相对应。
  • 我们使用[XGYModel setValuesForKeysWithDictionary:dict];进行字典转模型。
    字典转模型的原理:
    // enumerateKeysAndObjectsUsingBlock:遍历字典中的所有keys和valus
[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
    // 利用KVC给模型中属性赋值,,
    // key:用来给哪个属性
    // Value:给模型的值
    [XGYModel setValue:obj forKey:key];
}];
4. 修改内部控件的私有属性

比如自定义UITabbar,UIPageControl ....
//后续有待补充...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容