KVC
- kvc全称key-value-coding(键值编码),通常是用来给某一个对象的属性进行赋值,比如有一个person类,其对外有3个属性 —— 姓名、性别和年龄,我们创建一个人
p
后可以通过点语法直接给p的属性赋值。
// 创建person对象
Person *p = [[Person alloc] init];
// 给person对象属性赋值
p.name = @"狗蛋儿";
p.sex = @"男";
p.age = 188;
-
接着我们通过kvc的方式给
p
对象的属性赋值- 注意:因为setValue中的值是id类型,指向任何对象,所以需要将整数包装成一个对象
[p setValue:@"狗蛋儿" forKey:@"name"]; [p setValue:@"男" forKey:@"sex"]; [p setValue:@188 forKey:@"age"];
- 注意:因为setValue中的值是id类型,指向任何对象,所以需要将整数包装成一个对象
到这里可能有的朋友会说这样赋值麻烦而且多此一举,但是如果person这个类中有个私有的属性 ——
height
,只提供输出接口,那么正常情况下我们是无法改变其值的,如下:
.h
@interface Person : NSObject
/**
* 获取身高
*/
- (void)getHeight;
@end
.m
@implementation Person
{
NSInteger _height; // 身高
}
- (void)getHeight {
NSLog(@"身高--%ld", _height);
}
@end
- 这个时候外界是无法访问到
_height
这个属性的,但是如果想改变其值,那么我们就可以使用KVC的方式
[p setValue:@1.8f forKey:@"height"];
// 获取身高
[p getHeight];
结果:
-
除了
setValue:forKey:
这个方法外,苹果还提供了另一个方法setValue:forKeyPath:
这两个方法对于普通属性来说是没有区别的,都可用,但是对于一些特殊的属性自然就要使用setValue:forKeyPath:
方法了(看名字都这么长,肯定比较牛)至于牛仔哪里,肯定要试一试- 假如person这个类中我们又有个属性dog,Dog类中又有个属性体重,那么我们怎么通过'p'这个对象去设置狗的属性呢?
// 初始化Dog对象 p.dog = [[Dog alloc] init]; // 给dog对象赋值 [p setValue:@"旺财" forKey:@"dog.name"];
结果:
- 从图中可以看到,如果我们使用
setValue:forKey:
这个方法,Xcode会报错说找不到dog.name这个key,想想我们在stroyboard中,如果我们控件连线出现错误,也会报相似的错误,说明了stroyboard在赋值的时候也是通过kvc的方式来操作的 - 现在我们来试试用
setValue:forKeyPath:
方法
[p setValue:@"旺财" forKeyPath:@"dog.name"];
结果:
- 成功了,说明
setValue:forKeyPath:
方法中包含了setValue:forKeyPath:
的方法,但是内部增加了更高级的功能 —— 内部实现:它会先去person
类中寻找有没有dog
这个key,如果有,那么会去Dog
类中寻找有没有name
这个key,如果有,就给name
这个key赋值,所以个人比较喜欢使用setValue:forKeyPath
这个方法
不知道大家有没有注意到,前面我们使用KVC的方式给
_height
属性赋值的时候我们是这样写的
[p setValue:@1.8f forKey:@"height"];
-
这边有个问题 —— 我们传入的字符串key是
height
,但是定义的属性则是_height
,为什么还可以给_height
赋值呢?- 这说明使用KVC对某个属性进行赋值时,可以不用加
_
,因为KVC的查找规则是:先寻找和直接输出的字符串相同的成员变量,如果找不到再去寻找以_
开头的相似的成员变量
- 这说明使用KVC对某个属性进行赋值时,可以不用加
-
当然,KVC的用处不仅仅这点,开发中我们经常需要将字典转成模型以方便View取值,这里我们给
Person
类再提供一个外部方法,在方法中实现使用KVC的方式将传入的字典转成模型- 当然,
setValuesForKeysDictionary:
这个方法只能实现比较简单的字典转模型,如果是深层次的字典,还是需要手动去实现,也可以使用第三方框架来处理(业内用的最多的应该就是MJExtension,不得不说确实好用)
- 当然,
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
- 接着我们在外部调用
initWithDict:
这个方法来实现字典转模型
// 初始化字典内容
NSDictionary *dict = @{
@"name" : @"狗蛋儿",
@"sex" : @"男",
@"age" : @188,
};
// 调用initWithDict:方法
Person *p1 = [p initWithDict:dict];
// 打印
NSLog(@"p1.name=%@, p1.sex=%@, p1.age=%ld", p1.name, p1.sex, p1.age);
结果:
-
前面只提到赋值,但是怎么取值呢,其实很简单,苹果提供了2个取值的方法,取值时只需要将对应的key传入就可以了
valueForKey:
valueForKeyPath:
-
总结:
- KVC常见的2种应用场景
- 对私有变量进行赋值
- 字典转模型
- 字典转模型注意点
- 字典中的某个key一定要在模型中有对应属性
- 一个模型中如果包含了另外的模型对象,是无法直接使用
setValuesForKeysDictionary:
方法转化成功的 - 通过KVC转化模型中你的模型,也是无法直接转化成功的
- KVC常见的2种应用场景
KVO
- KVO全称key-value-observing,也就是我们常说的观察者模式,它的原理就是利用一个key来找到某个属性并监听值的改变
- KVO的使用比较简单,大致分为3个步骤
- 添加观察者
- 在观察者中实现监听方法
- 移除观察者
1.添加观察者
// 实例化person对象
_p = [[Person alloc] init];
_p.name = @"狗蛋儿";
/**
* 观察p对象的name值,如果改变则打印新值
*
* @param observer 观察者
* @param keyPath 观察的属性
* @param options 观察模式
* @param context 额外数据
*/
// options取值:NSKeyValueObservingOptionNew(新值), NSKeyValueObservingOptionOld(旧值)
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
2.在回调中实现监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
NSLog(@"%@的值改变为%@",keyPath, change[@"new"]);
}
3.移除观察者(重要)
- (void)dealloc {
// 移除
[_p removeObserver:self forKeyPath:@"name"];
}
结果:
那么KVO实现原理:当观察一个对象时,一个新的类会动态被创建,这个类继承自该对象原本的类,并且重写了被观察的属性的setter方法,重写的setter方法会负责在调用原setter方法之前和之后,通知所有观察对象,最后把这个对象的isa指针指向这个新创建的子类,对象就变成新创建的子类的实例,苹果为了让我们认为这个类没有被修改过,还重写了-class方法(就是原本的类),至于为什么这样做,还没弄清楚,苹果也不希望暴露太多KVO实现细节,苹果官方文档只说明:被观察对象的isa指针会指向一个中间类,而不是原来真正的类
KVO的缺陷:KVO是很强大的,但是苹果给的API实在是不咋地,我们只能通过重写
observeValueForKeyPath:ofObject:change:context:
方法获得通知,想传block、自定义方法等都不行,所以实现开发中KVO使用的情景不多,更多是用Delegate(代理)或者NotificationCenter(通知中心)代替,当然很多大神已经吊打官方KVO好多次了,有想研究了可以去搜一下相关资料
最后附上本章参考Demo 密码: b55p