iOS的Key-Value Coding

KVC(Key-Value Coding)键值编码,由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象符合kvc时,可以通过简介,统一的消息传递接口通过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。就是指iOS的开发中,可以允许开发者通过key变量名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。

KVC是许多其他Cocoa技术的基础概念,例如 KVOCocoa bindings, Core Data, 和AppleScript-ability

在OC中,对象从NSObject(直接或间接)继承时,通常都是实现了NSKeyValueCoding协议,又为基本方法提供默认方法的实现,例如可以通过valueForKey:方法和setValue:forKey:方法来分别获取参数的值和设置参数的值。

NSKeyValueCoding

接下来为了更好地对KVC的说明,定义了一个User的对象
User.h的代码:

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

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;


@end

KVC的Getter查找模式

通过valueForKey:方法给定一个key参数作为输入的默认实现,它的调用的类实例内部执行过程(其中key为键字符串,相当于成员变量名)

1.首先搜索实例的getter方法,按照get<Key><key>is<Key>_<key>这些get方法来查找的,如果找到则调用它并执行步骤5。否则继续下一步。

2.如果没有找到步骤1中的简单访问器方法,那么会在实例中搜索countOf<Key>objectIn<Key>AtIndex:相关的两个方法是否有实现,如果有找到这两个方法,则会以一个NSArray的数组形式返回,如果找不到就继续执行步骤3

3.如果以上两个步骤都没有找到的话,就会寻找三个方法,分别是countOf<Key>enumeratorOf<Key>memberOf<Key>:,如果这个三个方法找到的话,就可以创建一个NSSet的集合代理对象返回,此代理对象随后将其收到的任何NSSet消息转换为countOf <Key>,enumeratorOf <Key>和memberOf <Key>:消息的某种组合,以创建它的对象。 实际上,代理对象与与键值编码兼容的对象一起使用,类型于这个对象就像是NSSet一样,否则执行步骤4.

4.执行到这个步骤的时候,会先找到类方法accessInstanceVariablesDirectly,如果返回YES(默认是yes),就会搜索实例的变量名_<key>,_is<Key>,<key>,is<Key>这种顺序来查找。如果找到,直接获取实例变量的值,然后继续执行步骤5,否则执行步骤6.

5.如果检索到属性值是对象指针,则返回结果。如果该值是NSNumber类型,则将其存储为NSNumber类型并返回,如果不是则转换为NSValue对象并返回对象。

6.如果以上的所有方法都查找不到,这时会调用valueForUndefinedKey:,默认情况下会引发NSUndefinedKeyException异常,但是可以在NSObject的子类中重写这个方法来做一些处理防止程序闪退。

根据上面的步骤来验证getter的查找模式

以下是部分代码的实现:

    User *user = [[User alloc] init];
    user.arr = @[@"name0", @"name1", @"name2", @"name3"];
    [user setValue:@"张三" forKey:@"name"];
    NSString *name = [user valueForKey:@"name"];
    NSLog(@"name的值:%@",name);

在User.m的代码里面

-(NSString *)getName{
    return @"getName";
}

-(NSString *)name{
    return @"name";
}

-(NSString *)isName{
    return @"isName";
}

-(NSString *)_name{
    return @"_name";
}

通过执行代码并且按照步骤1中的顺序来对部分代码注释可以发现get方法的执行结果分别是

[8389:179602] name的值:我是getName方法
[8450:181614] name的值:我是name方法
[8476:182555] name的值:我是isName方法
[8503:183568] name的值:我是_name方法

所以通过以上的结果知道getter的执行顺序是getName()-->name()-->isName()-->_name()。
为了对步骤2,步骤3和步骤4进行验证在User.m中添加了如下代码:

// 个数
- (NSUInteger)countOfName{
    NSLog(@"%s",__func__);
    return [self.arr count];;
}

//// 获取值
- (id)objectInNameAtIndex:(NSUInteger)index {
    NSLog(@"%s",__func__);
    return [NSString stringWithFormat:@"name %lu", index];
}

// 是否包含这个成员对象
- (id)memberOfName:(id)object {
    NSLog(@"%s",__func__);
    return [self.set containsObject:object] ? object : nil;
}

// 迭代器
- (id)enumeratorOfName {
    // objectEnumerator
    NSLog(@"来了 迭代编译");
    return [self.arr reverseObjectEnumerator];
}
+(BOOL)accessInstanceVariablesDirectly{
    return YES;
}

通过注释掉getter里面的方法,会发现是先执行countOfNameobjectInNameAtIndex这两个方法的,执行结果如下:

[8727:189400] -[User countOfName]
[8727:189400] -[User countOfName]
[8727:189400] -[User objectInNameAtIndex:]
[8727:189400] -[User objectInNameAtIndex:]
[8727:189400] -[User objectInNameAtIndex:]
[8727:189400] -[User objectInNameAtIndex:]
[8727:189400] name的值:(
    "name 0",
    "name 1",
    "name 2",
    "name 3"
)

当将objectInNameAtIndex这个方法注释掉之后,就会执行memberOfNameenumeratorOfName。当然如果这些执行的步骤中有某一个方法没有实现的话,都会直接走到下一个步骤的,所以这些方法是缺一不可的。
当对步骤4验证的时候就需要将countOfName``objectInNameAtIndex``memberOfNameenumeratorOfName等方法注释掉,并且User的实现方法改为:

    User *user = [[User alloc] init];
    [user setValue:@"张三" forKey:@"name"];
    NSString *name = [user valueForKey:@"name"];
    NSLog(@"name的值:%@",name);
    NSLog(@"获取到的值:_name:%@,_isName:%@,name:%@,isName:%@",
          user->_name,user->_isName,user->name,user->isName);
    NSLog(@"获取到的值:_isName:%@,name:%@,isName:%@",user->_isName,user->name,user->_isName);
    NSLog(@"获取到的值:name:%@,isName:%@",user->name,user->_isName);
    NSLog(@"获取到的值:_isName:%@",user->_isName);

User.h的部分代码:

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

从而得到在accessInstanceVariablesDirectly返回YES的时候(默认是YES)查找的顺序是_name-->_isName-->name-->isName,当accessInstanceVariablesDirectly为NO的时候直接就报valueForUndefinedKeyNSUndefinedKeyException异常,当然也可以直接在User里面实现valueForUndefinedKey对异常做处理。到此就结束了KVC对成员变量的查找的验证。

KVC的Setter的查找模式

相对于getter的查找模式来说,setter的查找模式就简单很多了。
通过setValue:forKey:,给定的keyvalue作为参数输入,其搜索的是以下步骤:

1.先按set<Key>_set<Key>这个顺序来查找方法,如果找到就设置值做相应的操作

2.如果没有找到简单的访问方法,先找到类方法accessInstanceVariablesDirectly,如果返回YES(默认返回yes),查找实例变量的名称,按照_<key>,_is<Key>,<key>,is<Key>这个顺序来查找,如果找到就可以设置变量并完成操作

3.在以上两种步骤都没有实现的情况下,就会调用setValue:forUndefinedKey:,默认情况下会引发异常,但是NSObject的子类可以实现这个方法来防止报异常。
这里就不做一一验证了,与上面的Getter是差不多的。

KVC的keyPath

在开发过程中,一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC获取该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径keyPath

用到的两个方法

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

简单的用法,例如定义一个Student类


@interface Student : NSObject

@property (nonatomic, copy)   NSString          *name;
@property (nonatomic, copy)   NSString          *nick;
@property (nonatomic, assign) int               age;

@end

然后这个Student在User类中作为属性,实现为:

    User *user = [[User alloc] init];
    Student *student = [[Student alloc] init];
    user.student = student;
    [user setValue:@"学生" forKeyPath:@"student.name"];
    NSLog(@"%@",[user valueForKeyPath:@"student.name"]);

这就是keyPath的简单用法

设置非对象值为nil

在KVC中设置非对象值为nil的时候,例如:

Student *student = [[Student alloc] init];
[student setValue:nil forKey:@"age"];

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<Student 0x6000005273c0> setNilValueForKey]: could not set nil as the value for the key age.'

这是会报NSInvalidArgumentException异常,主要实现了-(void)setNilValueForKey:(NSString *)key方法就不会有异常了,也可以在该方法中做一些处理。但是如果对Student中的name属性设置为nil的时候[student setValue:nil forKey:@"name"];却不会报错,这是为什么呢?通过去看这个方法的说明

/* Given that an invocation of -setValue:forKey: 
would be unable to set the keyed value because the type of the parameter of the corresponding 
accessor method is an NSNumber scalar type or NSValue structure type 
but the value is nil, set the keyed value 
using some other mechanism. 
The default implementation of this method raises an NSInvalidArgumentException. 
You can override it to map nil values to 
something meaningful in the context of your application.
*/
- (void)setNilValueForKey:(NSString *)key;

了解到这里大概的说法就是只对NSNumber和NSValue的结构体等类型的数据在setvalue的时候为nil才会报错。

KVC访问非对象值

在User类中添加一个结构体,并且这个结构体是User类中的一个属性

typedef struct{
    float x, y, z;
} ThreeFloats;

如果类似一般情况下直接设置值的话,是直接就报错的了

image.png

在上文中有提到,在Getter的查找模式中,步骤5中讲到,如果查找值,找到的是NSNumber类型的就以NSNumber的返回,但是如果例如类似结构体,CGSize,CGRect等这些类型的,只能转换为NSValue的形式设置值,当然对结构体的获取值也是需要转换的。

    User *user = [[User alloc] init];
    ThreeFloats floats = {1., 2., 3.};
    NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [user setValue:value forKey:@"threeFloats"];
    NSValue *reslut = [user valueForKey:@"threeFloats"];
    NSLog(@"%@",reslut);
    ThreeFloats th;
    [reslut getValue:&th] ;
    NSLog(@"%f - %f - %f",th.x,th.y,th.z);

[68376:588080] {length = 12, bytes = 0x0000803f0000004000004040}
[68376:588080] 1.000000 - 2.000000 - 3.000000

至此有关kvc的常用的介绍就到这里了,当然这只是简单地介绍了部分有关kvc的内容,还有更多的有关可变数组,可变集合和可变有序集等搜索模式可以去苹果官网键值编码编程指南去了解更多。

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

推荐阅读更多精彩内容