说一下 KVC 和 KVO

本篇采用简单的例子,来介绍 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 。若直接访问属性对象则没有效果。