[关闭]
@MicroCai 2016-05-20T12:09:46.000000Z 字数 8828 阅读 21295

KVO 和 KVC 的使用和实现

Archives iOS


了解 KVO/KVC

KVO/KVC 是观察者模式在 Objective-C 中的实现,以非正式协议(Category)的形式被定义在 NSObject 中。从协议的角度看,是定义了一套让开发者遵守的规范和使用的方法。在 Cocoa 的 MVC 框架中,架起 ViewController 和 Model 沟通的桥梁。

  1. // "NSKeyValueObserving.h"
  2. @interface NSObject(NSKeyValueObserving)
  3. // "NSKeyValueCoding.h"
  4. @interface NSObject(NSKeyValueCoding)

KVO 即 Key-Value-Observing,顾名思义用于观察键值

  1. //通过此方法即可添加对象的观察者
  2. - (void)addObserver:(NSObject *)observer
  3. forKeyPath:(NSString *)keyPath
  4. options:(NSKeyValueObservingOptions)options
  5. context:(void *)context;

KVC 即 Key-Value-Coding,用于键值编码

  1. - (id)valueForKey:(NSString *)key;
  2. - (void)setValue:(id)value forKey:(NSString *)key;
  3. - (id)valueForKeyPath:(NSString *)keyPath;
  4. - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

如何使用 KVO/KVC

简单使用

为了让各位童鞋更好的理解,下面主要使用例子讲解 KVO/KVC 的使用

先定义一个 Persson 类,当做被观察的对象

  1. //Person.h
  2. #import <Foundation/Foundation.h>
  3. @interface Person : NSObject
  4. @end
  1. // Person.m
  2. #import "Person.h"
  3. @interface Person () {
  4. NSString *address; //地址
  5. CGFloat weight; //体重
  6. }
  7. @property (nonatomic, copy) NSString *name; //名字
  8. @property (nonatomic, assign) NSInteger age; //年龄
  9. @end
  10. @implementation Person
  11. @end

再定义一个 PersonObserver 类,用于观察 Person

  1. //PersonObserver.h
  2. #import <Foundation/Foundation.h>
  3. @interface PersonObserver : NSObject
  4. @end
  1. //PersonObserver.m
  2. #import "PersonObserver.h"
  3. @implementation PersonObserver
  4. //观察者需要实现的方法
  5. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  6. {
  7. NSLog(@"old: %@", [change objectForKey:NSKeyValueChangeOldKey]);
  8. NSLog(@"old: %@", [change objectForKey:NSKeyValueChangeNewKey]);
  9. NSLog(@"context: %@", context);
  10. }
  11. @end

观察者和被观察者准备就绪,即可进行测试

  1. // 测试 KVO & KVC
  2. - (void)testMethod
  3. {
  4. Person *aPerson = [[Person alloc] init];
  5. PersonObserver *aPersonObserver = [[PersonObserver alloc] init];
  6. //添加观察者
  7. //也可以观察 age、address、weight
  8. [aPerson addObserver:aPersonObserver
  9. forKeyPath:@"name"
  10. options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
  11. context:@"this is a context"];
  12. //设置key的value值,aPersonObserver接收到通知
  13. [aPerson setValue:@"LiLei" forKey:@"name"];
  14. NSLog(@"name: %@", [aPerson valueForKey:@"name"]);
  15. //移除观察者
  16. [aPerson removeObserver:aPersonObserver forKeyPath:@"name"];
  17. }

当代码执行到第16行时,aPersonObserver 接收到通知,打印 changecontext 值。整个代码的执行结果如下:

  1. 2015-01-30 01:26:41.997 HelloWorld[4132:1588732] old: <null>
  2. 2015-01-30 01:26:41.997 HelloWorld[4132:1588732] new: LiLei
  3. 2015-01-30 01:26:41.997 HelloWorld[4132:1588732] context: this is a context
  4. 2015-01-30 01:26:41.997 HelloWorld[4132:1588732] name: LiLei

如果 Person 类里面还有个 Job 的属性

  1. @property (nonautomic, strong) Job *aJob; //工作
  1. #import <Foundation/Foundation.h>
  2. @interface Job : NSObject
  3. @property (nonautomic, copy) NSString *companyName; //公司名字
  4. @property (nonautomic, assign) CGFloat salary; //薪水
  5. @end

要观察和设置 Person 的薪水,只要这么写就可以

  1. // 观察 salary
  2. [aPerson addObserver:aPersonObserver
  3. forKeyPath:@"aJob.salary"
  4. options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
  5. context:@"this is a context"];
  6. //设置月薪:20k
  7. [aPerson setValue:@"20000.0" forKey:@"aJob.salary"];

操作集合

如果 Person 需要车,就添加 cars 属性,于是就有了很多车

  1. @property (nonautomic, copy) NSArray *cars; //很多车

NSArray *cars 这种有序集合属性的操作有两种方法

  1. /**
  2. * 有序集合的操作
  3. * 将所有方法 <Key> 替换成 Cars,且首字母大写
  4. */
  5. //必须实现,对应于NSArray的基本方法count:
  6. -countOf<Key>
  7. //这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
  8. -objectIn<Key>AtIndex:
  9. -<Key>AtIndexes:
  10. //不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:
  11. -get<Key>:range:
  12. //两个必须实现一个,类似 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
  13. -insertObject:in<Key>AtIndex:
  14. -insert<Key>:atIndexes:
  15. //两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
  16. -removeObjectFrom<Key>AtIndex:
  17. -remove<Key>AtIndexes:
  18. //可选的,如果在此类操作上有性能问题,就需要考虑实现之
  19. -replaceObjectIn<Key>AtIndex:withObject:
  20. -replace<Key>AtIndexes:with<Key>:

相对应的,像 NSSet 这种无序集合同样也有如下的方法可以使用

  1. /**
  2. * 无序集合的操作
  3. * 将所有方法 <Key> 替换成 Cars,且首字母大写
  4. */
  5. //必须实现,对应于NSArray的基本方法count:
  6. -countOf<Key>
  7. //这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
  8. -objectIn<Key>AtIndex:
  9. -<key>AtIndexes:
  10. //不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:
  11. -get<Key>:range:
  12. //两个必须实现一个,类似 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
  13. -insertObject:in<Key>AtIndex:
  14. -insert<Key>:atIndexes:
  15. //两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
  16. -removeObjectFrom<Key>AtIndex:
  17. -remove<Key>AtIndexes:
  18. //这两个都是可选的,如果在此类操作上有性能问题,就需要考虑实现之
  19. -replaceObjectIn<Key>AtIndex:withObject:
  20. -replace<Key>AtIndexes:with<Key>:

但是,如果要使用这些方法,开发者需自己实现一遍,所以使用上相对麻烦。

键值验证(KVV,Key-Value Validate)

KVC 提供了键值验证(KVV)机制,让开发者有机会能够挽回错误,保证数据的一致性。开发者需先调用下面

  1. - (BOOL)validateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

这个方法会默认调用的实现方法如下

  1. - (BOOL)validate<Key>:error:

举个例子,上面的例子中,希望能够验证 Person 的属性 name 不为空,就可以这么写

  1. // 测试 KVV
  2. - (void)testMethod
  3. {
  4. Person *aPerson = [[Person alloc] init];
  5. PersonObserver *aPersonObserver = [[PersonObserver alloc] init];
  6. //添加观察者
  7. [aPerson addObserver:aPersonObserver
  8. forKeyPath:@"name"
  9. options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
  10. context:@"this is a context"];
  11. NSString *name = @"LiLei";
  12. NSString *key = @"name";
  13. NSError *error = nil;
  14. // KVV 验证
  15. BOOL isLegalName = [aPerson validateValue:&name forKey:key error:&error];
  16. if (isLegalName) {
  17. NSLog(@"it's a legal name.");
  18. [aPerson setValue:name forKey:key];
  19. }else{
  20. NSLog(@"the name is illegal.");
  21. }
  22. //移除观察者
  23. [aPerson removeObserver:aPersonObserver forKeyPath:@"name"];
  24. }
  1. //Person.m
  2. //Person.m 文件新增验证名字的KVV方法
  3. @implementation Person
  4. - (BOOL)validateName:(NSString **)name error:(NSError * __autoreleasing *)outError
  5. {
  6. if ((*name).length == 0)
  7. {
  8. (*name) = @"default name";
  9. return NO;
  10. }
  11. return YES;
  12. }
  13. @end

集合操作符(Collection Operator)

集合运算符是一种特殊的 key path,通过 - (id)valueForKeyPath:(NSString *)keyPath 方法获取集合中的信息,其格式如下:

集合运算符(Collection Operator)

由数值组成的集合,总共有 5 中操作符

使用示例

  1. //假设在 Person 类中还有个存储数值的数组 array
  2. //获取array 中的所有数值的和
  3. CGFloat sum = [aPerson valueForKeyPath:@"@sum.array"];
  4. //获取 array 中所有数值的平均数
  5. CGFloat avg = [aPerson valueForKeyPath:@"@avg.array"];

由对象组成的集合有 2 种操作符

由数组组成的集合(集合中有集合)有如下 3 种操作符。

由于 Set 中的元素本身就是不重复的,所以没有 unionOfSets 操作符。

手动键值观察

通过自动属性,建立键值观察,都属于自动键值观察。因为使用这种方法,只要设置键值,就会自动发出通知。而手动键值观察,不能使用自动化属性,需要自己写 setter/getter 方法,手动发送通知。

  1. //手动通知的实现
  2. @interface Person : NSObject
  3. {
  4. NSString *name;
  5. }
  6. - (NSString *)name;
  7. - (void)setName:(int)theName;
  8. @end
  9. @implementation Person
  10. - (id) init
  11. {
  12. self = [super init];
  13. if (nil != self) {
  14. name = @"LiLei";
  15. }
  16. return self;
  17. }
  18. - (NSString *)name
  19. {
  20. return name;
  21. }
  22. - (void)setName:(NSString *)theName
  23. {
  24. //发送通知:键值即将改变
  25. [self willChangeValueForKey:@"name"];
  26. name = theName;
  27. //发送通知:键值已经修改
  28. [self didChangeValueForKey:@"name"];
  29. }
  30. /**
  31. * 当设置键值之后,通过此方法,决定是否发送通知
  32. */
  33. + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key
  34. {
  35. //当 key 为 name时,手动发送通知
  36. if ([key isEqualToString:@"age"]) {
  37. return NO;
  38. }
  39. //当为其他key时,自动发送通知
  40. return [super automaticallyNotifiesObserversForKey:key];
  41. }
  42. @end

设置属性之间的依赖

假如有个 Person 类,类里有三个属性,fullNamefirstNamelastName。按照之前的知识,如果需要观察名字的变化,就要分别添加 fullNamefirstNamelastName 三次观察,非常麻烦。如果能够只观察 fullName,并建立 fullNamefirstNamelastName 的某种依赖关系,当发生变化时,也受到通知,那该多好啊!

KVC 刚好提供这种键之间的依赖方法,格式如下

  1. + (NSSet *)keyPathsForValuesAffecting<Key>;

这方法使得 Key 之间能够建立依赖关系,为了便于说明,直接用 属性依赖 这个词代替 Key 之间的依赖。含义不同,结果一致。

下面就使用这种方法解决 Key 之间的依赖关系。

Person 类为被观察者

  1. //Person.h
  2. #import <Foundation/Foundation.h>
  3. @interface Person : NSObject
  4. @end
  1. //Person.m
  2. #import "Person.h"
  3. @interface Person ()
  4. @property (nonatomic, copy) NSString *fullName; //名字,依赖于firstName、lastName
  5. @property (nonatomic, copy) NSString *firstName;
  6. @property (nonatomic, copy) NSString *lastName;
  7. @end
  8. @implementation Person
  9. //设置属性依赖:fullName属性依赖于firstName、lastName
  10. //如果观察name,当firstName、lastName发生变化时,观察者也会收到name变化通知
  11. + (NSSet *)keyPathsForValuesAffectingFullName
  12. {
  13. NSSet *set = [NSSet setWithObjects:@"firstName", @"lastName", nil];
  14. return set;
  15. }
  16. @end

PersonObserver 类为观察者

  1. //PersonObserver.h
  2. #import <Foundation/Foundation.h>
  3. @interface PersonObserver : NSObject
  4. @end
  1. //PersonObserver.m
  2. #import "PersonObserver.h"
  3. @implementation PersonObserver
  4. //观察者需要实现的方法
  5. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  6. {
  7. NSLog(@"observer receive change infomation");
  8. }
  9. @end

准备就绪,就可以测试依赖的属性了

  1. - (void)testMethod
  2. {
  3. Person *aPerson = [[Person alloc] init];
  4. PersonObserver *aPersonObserver = [[PersonObserver alloc] init];
  5. //观察fullName属性
  6. [aPerson addObserver:aPersonObserver
  7. forKeyPath:@"fullName"
  8. options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
  9. context:@"this is a context"];
  10. //设置firstName时,aPersonObserver仍然会收到name变化的通知
  11. [aPerson setValue:@"LiLei" forKey:@"firstName"];
  12. //移除观察者
  13. [aPerson removeObserver:aPersonObserver forKeyPath:@"fullName"];
  14. }

输出结果,发现虽然观察的是 fullName,但是当修改 firstName 的时候,观察者也会收到 fullName 变化的通知,达到了我们的期望。

  1. 2015-01-30 06:32:20.527 HelloWorld[28005:130136] observer receive change infomation

理解 KVO/KVC 的实现

KVO 是通过 isa-swizzling 实现的。

基本的流程就是编译器自动为被观察对象创造一个派生类,并将被观察对象的isa 指向这个派生类。如果用户注册了对某此目标对象的某一个属性的观察,那么此派生类会重写这个方法,并在其中添加进行通知的代码。Objective-C 在发送消息的时候,会通过 isa 指针找到当前对象所属的类对象。而类对象中保存着当前对象的实例方法,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法。由于编译器对派生类的方法进行了 override,并添加了通知代码,因此会向注册的对象发送通知。注意派生类只重写注册了观察者的属性方法。


参考资料
[1] Apple Documentation: Key-Value Coding Programming Guide
[2] objc中国 - 卢思豪:KVC 和 KVO
[3] wangzz:KVC/KVO原理详解及编程指南
[4] 飘飘白云:深入浅出Cocoa 详解键值观察(KVO)及其实现机理
[5] iliunian:iOS编程——Objective-C KVO/KVC机制
[6] 低下自己的心:object-c编程tips-KVO,KVC浅析

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注