本篇采用简单的例子,来介绍 iOS 中的 KVC 和 KVO 的用法和实现原理。
一、KVC
1. KVC是什么
KVC 即 Key-Value Coding,翻译成键值编码。它是一种不通过存取方法,而通过属性名称字符串间接访问属性的机制。
2. KVC的用法
KVC 常用到的方法有下面几个:
1 2 3 4 5
| - (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;
|
前面的两个方法,以字符串的形式传入对象属性即可调用。私有属性也可以调用。如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
@interface ObjectA : NSObject
@property (nonatomic, strong) NSString *publicPropertyString;
@end
@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
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ObjectA *objectA = [[ObjectA alloc] init];
NSLog(@"%@", [objectA valueForKey:@"publicPropertyString"]);
NSLog(@"%@", [objectA valueForKey:@"privatePropertyInteger"]);
[objectA setValue:@(999) forKey:@"privatePropertyInteger"];
NSLog(@"%@", [objectA valueForKey:@"privatePropertyInteger"]);
|
后面两个方法支持传入用 .
连接的多层级属性,比如 school.schoolmaster.name
。同样支持私有属性。如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@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
|
1 2 3 4 5 6 7 8
| ObjectB *objectB = [[ObjectB alloc] init];
[objectB setValue:@(999) forKeyPath:@"objectA.privatePropertyInteger"];
NSLog(@"%@", [objectB valueForKeyPath:@"objectA.privatePropertyInteger"]);
|
需要注意:
- 当
value
的值为基本类型时,应该封装为 NSNumber
或 NSValue
。
- KVC不会自动调用键值验证方法。当字符串中的属性值不存在时,会直接抛出异常。
- 可以先在类中重写
-validateValue: forKey: error:
,制定检查规则,然后手动调用该方法来验证。
- KVC的一个重要应用是字典转模型。
3. KVC的原理
为了设置或者获取对象属性,KVC按顺序使用如下技术:
- 获取对象属性时,检查是否存在
-<key>
、 -is<key>
(只针对布尔值有效)或者 -get<key>
的访问器方法,如果找到,就用这些方法来返回属性值;设置对象属性时,检查是否存在名为 -set<key>:
的方法,并使用它来设置属性值。对于 -get<key>
和 -set<key>:
方法,将大写Key字符串的第一个字母,并与Cocoa的方法命名保持一致。
- 如果上述方法找不到,则检查名为
-_<key>
、 -_is<key>
(只针对布尔值有效)、 -_get<key>
和 -_set<key>:
方法。
- 如果没有找到访问器方法,则尝试直接访问实例变量。实例变量可以是名为:
<key>
或 _<key>
。
- 如果仍未找到,则调用
valueForUndefinedKey:
和 setValue:forUndefinedKey:
方法。这些方法的默认实现都是抛出异常,可以根据需要重写它们。
可以看到,KVC会优先使用访问器方法来访问对象属性。
二、KVO
1. KVO是什么
KVO 即 Key-Value Observing,翻译成键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。
2. KVO的用法
KVO的使用主要分为三步:
第一步,将目标对象添加为观察者。(注意这里用到了KVC,即通过字符串的方式去访问属性值。)
1 2 3 4
| - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
|
第二步,实现接收通知的接口方法。
1 2 3 4
| - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
|
第三步,移除观察者。
1 2
| - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
|
在第一步中,NSKeyValueObservingOptions类型有四个取值,可以通过 |
来连接多个取值。分别为:
- NSKeyValueObservingOptionNew,在属性值变化的时候回调,可以在change中取到变化后的值。
- NSKeyValueObservingOptionOld,在属性值变化的时候回调,可以在change中取到变化前的值。
- NSKeyValueObservingOptionInitial,在属性值初始化或者变化的时候回调,拿不到变化前后的值。
- NSKeyValueObservingOptionPrior,在属性值变化前和变化后各回调一次,拿不到变化前后的值。
举一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @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<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([object isKindOfClass:[ObjectA class]] && [keyPath isEqualToString:@"privatePropertyInteger"]) { NSLog(@"%@", change); } 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 中,会添加以下两个方法的调用。
1 2
| - (void)willChangeValueForKey:(NSString *)key; - (void)didChangeValueForKey:(NSString *)key;
|
然后在 didChangeValueForKey:
中,去调用:
1 2 3 4
| - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
|
于是实现了属性值修改的通知。因为 KVO 的原理是修改 setter 方法,因此使用 KVO 必须调用 setter 。若直接访问属性对象则没有效果。
2. 重写class
当修改了isa指向后,class的返回值不会变,但isa的值则发生改变。
1 2 3 4 5 6 7
|
NSLog(@"%@", [_objectA class]);
NSLog(@"%@", object_getClass(_objectA));
|
3. 重写dealloc
系统重写 dealloc 方法来释放资源。
4. 重写_isKVOA
这个私有方法估计是用来标示该类是一个 KVO 机制声称的类。
参考