KVC/KVO
过去的靠现在忘记,将来的靠现在努力,现在才最重要。
1. KVC
KVC 也就是 key-value-coding,即键值编码,是一种间接访问对象实例变量(对象的属性)的方法机制。KVC 的主要用法如下。
1.1 通过键值路径为对象的属性赋值,主要是可以为私有的属性赋值
KVC 键值编码,通常是用来给某一个对象的属性进行赋值,例如有动物这么一个类,其对外有三个属性,类别、名字和年龄,我们在创建了一个动物 animal 后可以通过点语法直接给 animal 赋值。
// Animal.h文件
@interface Animal : NSObject
@property (nonatomic,strong)NSString *type;
@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)NSInteger age;
@end
// 创建对象并通过点语法赋值
Animal *animal = [[Animal alloc] init];
animal.type = @"dog";
animal.name = @"小白";
animal.age = 2;
我们也可以通过 KVC 给这个动物 animal 赋值,代码如下,因为 setValue 这里的值是 id 类型的,所以将整数包装成一个对象:
// KVC赋值
[animal setValue:@"cat" forKey:@"type"];
[animal setValue:@"小花猫" forKey:@"name"];
[animal setValue:@3 forKey:@"age"];
但是我们这样去赋值显得多此一举,可是如果人这个类的属性是没有暴露在外面呢?比如现在给动物这个类一个私有的体重的属性,并且对外提供一个输出体重的接口,如下:
// Animal.m文件
@implementation Animal {
NSInteger _weight;
}
- (void)logWeight {
NSLog(@"006 - KVC/KVO Weight = %ld",_weight);
}
@end
// 通过kvc直接对私有属性/变量进行赋值
[animal setValue:@15 forKey:@"weight"];
[animal logWeight];
// 输出结果
2021-08-22 21:36:03.040994+0800 001 - Class[2058:31772] 006 - KVC/KVO Weight = 15
针对代码 [animal setValue:@15 forKey:@"weight"]; 我们传入的字符串key 是 height,但是定义的属性是 _height,但是通过 KVC 还是可以给_height 属性赋到值。说明对某一个属性进行赋值,可以不用加下划线,而且它的查找规则应该是:先查找和直接写入的字符串相同的成员变量,如果找不到就找以下划线开头的成员变量。
通过 KVC 对对象属性赋值的两种方法比较:
除了 [animal setValue:<#(nullable id)#> forKey:<#(nonnull NSString *)#>] 这个方法外,还有一个方法也是可以对私有属性进行赋值的 [animal setValue:<#(nullable id)#> forKeyPath:<#(nonnull NSString *)#>],这两个方法对于一个普通的属性是没有区别的,都可以用,但是对于一些特殊的属性就有区别了。比如说动物这个类有个属性是所属主人(动物的拥有者,也是用一个类来描述),而人又有属性体重。
animal.people = [[People alloc] init];
[animal setValue:@70 forKey:@"people.weight"];
如果我们直接这样是会报错说找不到 people.weight 这个 key 的,而在storyboard 中,我们拖控件连线错误的时候也会报错说找不到什么 key,说明 storyboard 在赋值的时候也是通过 KVC 来操作的。
这里如果我们换另外的一个方法,这时候是不会报错的,而且可以打印出人的体重。
[animal setValue:@70 forKeyPath:@"people.weight"];
// 输出结果
2021-08-22 22:01:03.944144+0800 001 - Class[6806:55872] 006 - KVC/KVO type=cat name=小花猫 age=3 people.weith=70
说明 forKeyPath 是包含了 forKey 这个方法的功能的,甚至 forKeyPath 方法还有它自己的高级的功能,它会先去找有没有 people 这个 key,然后去找有没有 weight 这个属性。所以我们在使用 KVC 的时候,最好用 forKeyPath 这个方法。
1.2 通过键值路径获取属性的值,主要是可以通过 key 获得私有属性的值
// 通过KVC进行取值
NSLog(@"006 - KVC/KVO name=%@", [animal valueForKey:@"name"]);
NSLog(@"006 - KVC/KVO people.weight=%@", [animal valueForKeyPath:@"people.weight"]);
1.3 KVC 的另外一个用处:字典转模型
KVC 除了访问私有变量这个用处外,还可以用于字典转模型。在 Animal 类对外提供一个接口,将转模型的工作放在模型中进行。
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
外面可以直接将字典传入,和平常转模型相比,KVC 更加方便,减少了代码量。
// 字典转模型
NSDictionary *animalDict = @{@"type":@"dog", @"name":@"小黑",@"age":@"3"};
Animal *animal2 = [[Animal alloc] initWithDict:animalDict];
通过 KVC 字典转模型要注意以下几点:
字典转模型的时候,字典中的某一个 key 一定要在模型中有对应的属性,否则就会报错 NSUnknownKeyException。当然我们可以实现下面这个函数来解决这个问题:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
// 函数体可以为空
// 如果只是属性命名不一致,我们可以这样转换
if ([key isEqualToString:@"test"]) {
self.testID = value;
}
}
如果一个模型中包含了另外的模型对象,是不能直接转化成功的;
1.4 基于以上我们总结一下,KVC 的基本用处
1)通过键值路径为对象的属性赋值,主要是可以为私有的属性赋值;
2)通过键值路径获取属性的值,主要是可以通过 key 获得私有属性的值;
3)字典转模型;
4)修改一些系统控件的内部属性;例如设置:UITextField 中的placeHolderText
[textField setValue:[UIFont systemFontOfSize:25.0] forKeyPath:@"_placeholderLabel.font"];
如何获取控件内部的属性:
unsigned int count = 0;
objc_property_t *properties = class_copyPropertyList([UITextField class], &count);
for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];
const char *name = property_getName(property);
NSLog(@"name:%s",name);
}
5)高阶消息传递
当对容器类使用 KVC 时,valueForKey: 将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。
NSArray *arr = @[@"hubert", @"andy", @"cydia"];
NSArray *arrCap = [arr valueForKey:@"capitalizedString"];
for (NSString *str in arrCap) {
NSLog(@"%@",str);
}
KVC 对数组的作用机制
- 当对
NSArray调用valueForKey:时,KVC 会遍历数组中的每个元素,并对每个元素调用指定的key方法(此处为capitalizedString)。 capitalizedString是NSString的系统方法,作用是将字符串的首字母大写,其余字母小写(如@"hubert"→@"Hubert")。
打印结果,上述代码会输出以下内容:
Hubert
Andy
Cydia
- 原数组
@[@"hubert", @"andy", @"cydia"]中的每个字符串均被转换为首字母大写形式。
关键原理说明
- KVC 的集合操作特性:
valueForKey:在数组上的调用会自动映射到每个元素,相当于对每个元素执行[obj capitalizedString]。 - 系统方法兼容性:
capitalizedString是NSString的公开方法,KVC 可直接通过键名调用,无需额外实现。
此结果展示了 KVC 如何简化集合元素的批量操作。
6)KVC 中的函数操作集合
集合运算符: @avg @count @max @min @sum
@interface Book : NSObject
@property (nonatomic,assign) CGFloat price;
@end
NSArray* arrBooks = @[book1,book2,book3,book4];
NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
对象运算符
- @distinctUnionOfObjects
- @unionOfObjects
// 获取所有Book的price组成的数组,并且去重
NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
2. 观察者模式(KVO)
观察者模式是一种对象行为模式。它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。在观察者模式中,主体是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅并接收通知。观察者模式不仅被广泛应用于软件界面元素之间的交互,在业务对象之间的交互、权限管理等方面也有广泛的应用。
具体理解为 KVO 是键值观察者(key-value-observing),是苹果提供的一套事件通知机制(也叫做观察者模式)。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于 KVO 的实现机制,只针对属性才会发生作用,一般继承自NSObject 的对象都默认支持 KVO。
KVO 和 NSNotificationCenter 都是 iOS 中观察者模式的一种实现。KVO 对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO 可以监听单个属性的变化,也可以监听集合对象的变化。通过 KVC 的mutableArrayValueForKey: 等方法获得代理对象,当代理对象的内部对象发生改变时,会回调 KVO 监听的方法。集合对象包含 NSArray 和 NSSet。
2.1 KVO 基本使用
1)给对象的属性注册观察者:
// 注册观察者
[animal2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
2)在观察者中实现监听方法,observeValueForKeyPath: ofObject: change: context:(通过查阅文档可以知道,绝大多数对象都有这个方法,因为这个方法属于 NSObject):
/**
观察者监听的回调方法
@param keyPath 监听的keyPath
@param object 监听的对象
@param change 更改的字段内容
@param context 注册时传入的地址值
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
// 获取变化前后的值
NSLog(@"006 - KVO change=%@",change);
}
3)移除观察者:
- (void)dealloc {
[animal2 removeObserver:self forKeyPath:@"name"];
}
4)调用:
// 以下调用方式都可以触发KVO
animal2.name = @"新名字";
[animal2 setName:@"新名字1"];
[animal2 setValue:@"新名字2" forKey:@"name"];
[animal2 setValue:@"新名字3" forKeyPath:@"name"];
5)手动调用:
KVO 在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自己实现 KVO 属性的调用,则可以通过 KVO 提供的方法进行调用。下面以animal2 的 name 属性为例:
5.1)禁用自动调用:
// name 不需要自动调用,name 属性之外的自动调用
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = NO;
if ([key isEqualToString:@"name"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
// 单独设置某个属性
+ (BOOL)automaticallyNotifiesObserversOfName {
return NO;
}
针对每个属性,KVO都会生成一个 + (BOOL)automaticallyNotifiesObserversOfXXX 方法,返回是否可以自动调用KVO。按上面实现上述方法,我们会发现,此时改变 name 属性的值,无法触发KVO,还需要实现手动调用才能触发 KVO。
5.2)手动调用实现:
// KVO 手动调用实现
- (void)setName:(NSString *)name {
if (_name != name) {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
实现了(1)禁用自动调用(2)手动调用实现两步,name 属性手动调用就实现了,此时能和自动调用一样,触发 KVO。
6)Crash
KVO 若使用不当,极容易引发 Crash,下面总结使用过程中常见问题。
-
观察者未实现监听方法;
-
未及时移除观察者;
-
多次移除观察者;
7)属性依赖
// 属性依赖:如果属性type改变,观察者也能收到name改变的通知
+ (NSSet<NSString *> *)keyPathsForValuesAffectingName {
NSSet *set = [NSSet setWithObjects:@"type", nil];
return set;
}
8)监听集合对象的变化
首先,数组不能直接使用 KVO 使用监听。当我们想要使用 KVO 监听数组的状态时改变时,我们需要进行以下几步:
8.1)KVO 不能直接监听 UIViewController 中的数组变化。我们需要先创建一个模型,将需要监听的数组封装到模型中。然后控制器 UIViewController 持有模型对象,通过该对象才能监听。
@interface ObserveArray : NSObject
@property (nonatomic,strong)NSMutableArray *datas;
- (id)initWithDict:(NSDictionary *)dict;
@end
8.2)建立观察者与观察的对象,在观察者中实现监听方法
// ViewController.m 文件中
@interface ViewController ()
// 数组模型对象,来代替纯数组保存数据
@property (nonatomic,strong)ObserveArray *observeArray;
// 临时数组,用来接收被观察的模型中的数据,方便数据操作
@property (nonatomic,strong)NSMutableArray *tempArray;
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 创建观察的对象模型
NSDictionary *dict = [NSDictionary dictionaryWithObject:[NSMutableArray arrayWithCapacity:0] forKey:@"datas"];
_observeArray = [[ObserveArray alloc] initWithDict:dict];
[self KVOObserverArray];
}
// 给数组模型添加观察者
- (void)KVOObserverArray {
// 建立观察者以及观察者对象
[_observeArray addObserver:self forKeyPath:@"datas" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
// 模拟改变数据,这里不能直接[_observeArray.datas addobject"@""];
//[[_observeArray mutableArrayValueForKey:@"datas"] addObject:@"测试"];
_tempArray = [_observeArray mutableArrayValueForKey:@"datas"];
[_tempArray addObject:@"测试1"];
}
// 在观察者中实现监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"datas"]) {
NSLog(@"006 - KVO Array change=%@",change);
}
}
8.3)移除观察者
- (void)dealloc {
if (_observeArray != nil) {
[_observeArray removeObserver:self forKeyPath:@"datas"];
}
}
2.2 应用场景
- 下拉刷新、下拉加载监听 UIScrollView 的 contentoffsize;
- webview 混排监听 contentsize;
- 可以用来应用在导航栏颜色渐变;
- 监听模型属性实时更新 UI;
- 监听控制器 frame 改变,实现抽屉效果;
- 监听集合对象对象的变化;
- 监听带有状态的基础控件,如开关、按钮等;
- 当数据模型的数据发生改变时,视图组件能动态的更新,及时显示数据模型更新后的数据,比如 tableview 中数据发生变化进行刷新列表操作。
3. KVO/KVC、代理、通知的区别
3.1 与 KVC 的不同?
KVC,即是指 NSKeyValueCoding,一个非正式的 Protocol,提供一种机制来间接访问对象的属性,而不是通过调用 Setter、Getter 方法等显式的存取方式去访问。KVO 就是基于 KVC 实现的关键技术之一。
KVO,即 Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知。
3.2 与 delegate 的不同?
和 delegate 一样,KVO 和 NSNotification 的作用都是类与类之间的通信。但是与 delegate 不同的是:这两个都是负责发送接收通知,剩下的事情由系统处理,所以不用返回值;而 delegate 则需要通信的对象通过变量(代理)联系;delegate 只是一对一,而这两个可以一对多。delegate 是非常严格的语法,需要定义很多代码。
3.3 和 notification 的区别?
notification 比 KVO 多了发送通知的一步。两者都是一对多,但是对象之间直接的交互,notification 明显得多,需要 notificationCenter 来做为中间交互。而 KVO 如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了,只留一句“交由系统通知”,具体的可参照以上实现过程的剖析。notification 的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。
参考:https://www.sohu.com/a/343498712_671228
4. KVO 原理
当一个类的对象属性被观察时,系统会通过 runtime 动态的创建一个该类的派生类,并且会在这个类中重写基类被观察的属性的 setter 方法,并且在 setter 方法中实现了通知的机制。派生类重写了class方法,以“欺骗”外部调用者他就是原先那个类。而且系统将这个类的 isa 指针指向新的派生类,因此改对象也就是改新的派生类的对象了。从而实现了给监听的属性赋值时调用的是派生类的 setter 方法。从而激活键值通知机制,重写的 setter 方法会在调用原 setter方法前后,通知观察对象值得改变。此外派生类还重写了 delloc 方法来释放资源。


我们再看下详细的解析,全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。在 MVC 大行其道的 Cocoa 中,KVO 机制很适合实现 model 和 controller 类之间的通讯。
KVO 的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 A 时,KVO 机制动态创建一个对象 A 当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。
Apple 使用了 isa-swizzling (指针混写技术,来实现其内部查找定位的)来实现 KVO 。当观察对象 A 时,KVO 机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
A 类剖析:
NSLog(@"self->isa:%@",self->isa);
NSLog(@"self class:%@",[self class]);
在建立 KVO 监听前,打印结果为:
self->isa:A
self class:A
在建立 KVO 监听之后,打印结果为:
self->isa:NSKVONotifying_A
self class:A
在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_A 类,来实现当前类属性值改变的监听;
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类,就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A 的中间类,并指向这个中间类了。
子类 setter 方法剖析:
KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangeValueForKey: ,在存取数值的前后分别调用 2 个方法:
被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;
当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
- (void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; //KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO 在调用存取方法之后总调用
}
》https://sg.jianshu.io/p/47ead92286cd
》https://blog.csdn.net/qq_18505715/article/details/80205796
》https://www.jianshu.com/p/d412ac1113a0
5. Swift 中 KVO 的实现与限制
5.1 与 OC 的 KVO 对比
-
OC 的 KVO:基于
NSKeyValueCoding协议和动态派发(Runtime),所有继承自NSObject的类默认支持。 -
Swift 的 KVO:
默认不支持:Swift 属性默认关闭动态派发(静态派发优先),需显式添加
@objc dynamic修饰符开启运行时支持。依赖 NSObject:被观察类和观察者均需继承自
NSObject,否则无法使用 KVO。
5.2 Swift 实现 KVO 的条件
-
属性标记:被观察属性需添加修饰符
@objc dynamicclass Person: NSObject { @objc dynamic var age: Int = 0 // 支持 KVO var name: String = "" // 不支持 KVO } -
观察者注册:通过
addObserver(_:forKeyPath:options:context:)方法监听,并重写observeValue(forKeyPath:of:change:context:)处理变更。
5.3 替代方案:属性观察器(Property Observer)
-
Swift 原生特性:通过
willSet和didSet监听属性变化,无需继承NSObject。var score: Int = 0 { willSet { print("新值: \(newValue)") } didSet { print("旧值: \(oldValue)") } } -
局限性:仅能观察同一类内的属性,无法跨对象监听。
5.4 注意事项
- 性能影响:
dynamic会强制启用动态派发,可能降低性能。 - 类型限制:KVO 仅适用于类(Class),不适用于结构体(Struct)或枚举(Enum)。
Swift 中可通过 @objc dynamic 实现类似 OC 的 KVO,但更推荐优先使用属性观察器以减少运行时开销。

浙公网安备 33010602011771号