本篇采用简单的例子,来介绍 iOS 中的 KVC 和 KVO 的用法和实现原理。
一、KVC
1. KVC是什么
KVC 即 Key-Value Coding,翻译成键值编码。它是一种不通过存取方法,而通过属性名称字符串间接访问属性的机制。
2. KVC的用法
KVC 常用到的方法有下面几个:
- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
前面的两个方法,以字符串的形式传入对象属性即可调用。私有属性也可以调用。如下代码所示:
// 先声明一个对象ObjectA,同时具备私有属性和公有属性
// ObjectA.h
@interface ObjectA : NSObject
@property (nonatomic, strong) NSString *publicPropertyString;
@end
// ObjectA.m
@interface ObjectA ()
@property (nonatomic, assign) NSInteger privatePropertyInteger;
@end
@implementation ObjectA
- (instancetype)init {
self = [super init];
if (self) {
self.publicPropertyString = @"publicPropertyString";
self.privatePropertyInteger = 2000;
}
return self;
}
@end
// 尝试调用
ObjectA *objectA = [[ObjectA alloc] init];
// 以下输出:publicPropertyString
NSLog(@"%@", [objectA valueForKey:@"publicPropertyString"]);
// 以下输出:2000
NSLog(@"%@", [objectA valueForKey:@"privatePropertyInteger"]);
// 将999赋值给privatePropertyInteger
[objectA setValue:@(999) forKey:@"privatePropertyInteger"];
// 以下输出:999
NSLog(@"%@", [objectA valueForKey:@"privatePropertyInteger"]);
后面两个方法支持传入用 .
连接的多层级属性,比如 school.schoolmaster.name
。同样支持私有属性。如下代码所示:
// 再声明一个对象ObjectB,具备私有属性ObjectA
// ObjectB.m
@interface ObjectB ()
@property (nonatomic, strong) ObjectA *objectA;
@end
@implementation ObjectB
- (instancetype)init {
self = [super init];
if (self) {
self.objectA = [[ObjectA alloc] init];
}
return self;
}
@end
// 尝试调用
ObjectB *objectB = [[ObjectB alloc] init];
// 将999赋值给objectA的属性privatePropertyInteger
[objectB setValue:@(999) forKeyPath:@"objectA.privatePropertyInteger"];
// 以下输出:999
NSLog(@"%@", [objectB valueForKeyPath:@"objectA.privatePropertyInteger"]);
需要注意:
- 当
value
的值为基本类型时,应该封装为NSNumber
或NSValue
。 - KVC不会自动调用键值验证方法。当字符串中的属性值不存在时,会直接抛出异常。
- 可以先在类中重写
-validateValue: forKey: error:
,制定检查规则,然后手动调用该方法来验证。 - KVC的一个重要应用是字典转模型。
3. KVC的原理
为了设置或者获取对象属性,KVC按顺序使用如下技术:
- 获取对象属性时,检查是否存在
-
、-is
(只针对布尔值有效)或者-get
的访问器方法,如果找到,就用这些方法来返回属性值;设置对象属性时,检查是否存在名为-set
的方法,并使用它来设置属性值。对于: -get
和-set
方法,将大写Key字符串的第一个字母,并与Cocoa的方法命名保持一致。: - 如果上述方法找不到,则检查名为
-_
、-_is
(只针对布尔值有效)、-_get
和-_set
方法。: - 如果没有找到访问器方法,则尝试直接访问实例变量。实例变量可以是名为:
或_
。 - 如果仍未找到,则调用
valueForUndefinedKey:
和setValue:forUndefinedKey:
方法。这些方法的默认实现都是抛出异常,可以根据需要重写它们。
可以看到,KVC会优先使用访问器方法来访问对象属性。
二、KVO
1. KVO是什么
KVO 即 Key-Value Observing,翻译成键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。
2. KVO的用法
KVO的使用主要分为三步:
第一步,将目标对象添加为观察者。(注意这里用到了KVC,即通过字符串的方式去访问属性值。)
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
第二步,实现接收通知的接口方法。
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary *)change
context:(nullable void *)context;
第三步,移除观察者。
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath;
在第一步中,NSKeyValueObservingOptions类型有四个取值,可以通过 |
来连接多个取值。分别为:
- NSKeyValueObservingOptionNew,在属性值变化的时候回调,可以在change中取到变化后的值。
- NSKeyValueObservingOptionOld,在属性值变化的时候回调,可以在change中取到变化前的值。
- NSKeyValueObservingOptionInitial,在属性值初始化或者变化的时候回调,拿不到变化前后的值。
- NSKeyValueObservingOptionPrior,在属性值变化前和变化后各回调一次,拿不到变化前后的值。
举一个例子:
@interface ObjectB ()
@property (nonatomic, strong) ObjectA *objectA;
@end
@implementation ObjectB
- (instancetype)init {
self = [super init];
if (self) {
self.objectA = [[ObjectA alloc] init];
// 第一步,将目标对象添加为观察者
[_objectA addObserver:self
forKeyPath:@"privatePropertyInteger"
options:NSKeyValueObservingOptionNew
context:nil];
[_objectA setValue:@(999) forKey:@"privatePropertyInteger"];
}
return self;
}
// 第二步,实现接收通知的接口方法
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// 这里最好判断一下object的类型和keyPath的值,不符合则交给父类处理
if ([object isKindOfClass:[ObjectA class]] &&
[keyPath isEqualToString:@"privatePropertyInteger"]) {
NSLog(@"%@", change); // 这里可以读取到 new = 999
} else {
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
// 第三步,移除观察者。
- (void) dealloc {
[_objectA removeObserver:self
forKeyPath:@"privatePropertyInteger"];
}
@end
KVO可以在MVC模式中得到很好的应用。因为当Model发生变化时,通过KVO可以很方便地通知到Controller,从而通过Controller来改变View的展示。所以说KVO是解决Model和View同步的好办法。
3. KVO的原理
KVO的实现依赖于Runtime的强大动态能力。
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写这个类中任何被观察属性的 setter 方法。
即当一个类型为 ObjectA 的对象,被添加了观察后,系统会生成一个 NSKVONotifying_ObjectA 类,并将对象的isa指针指向新的类,也就是说这个对象的类型发生了变化。这个类相比较于ObjectA,会重写以下几个方法。
1. 重写setter
在 setter 中,会添加以下两个方法的调用。
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
然后在 didChangeValueForKey:
中,去调用:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary *)change
context:(nullable void *)context;
于是实现了属性值修改的通知。因为 KVO 的原理是修改 setter 方法,因此使用 KVO 必须调用 setter 。若直接访问属性对象则没有效果。
2. 重写class
当修改了isa指向后,class的返回值不会变,但isa的值则发生改变。
// 添加Observer之后
// 输出ObjectA
NSLog(@"%@", [_objectA class]);
// 输出NSKVONotifying_ObjectA(object_getClass方法返回isa指向)
NSLog(@"%@", object_getClass(_objectA));
3. 重写dealloc
系统重写 dealloc 方法来释放资源。
4. 重写_isKVOA
这个私有方法估计是用来标示该类是一个 KVO 机制声称的类。
参考
KVC和KVO的使用及原理
KVC/KVO原理详解及编程指南
iOS里的KVO模式
获取更佳的阅读体验,请访问原文地址 【Lyman's Blog】说一下KVC和KVO