Objective-c KVO and KVC
2014-02-25 10:31
465 查看
转载自: http://zhangbin.cc/archives/1839
个人认为这篇文章讲得比较清晰,所以就转了。感谢博主的奉献。
Objective-C Key-Value Coding 和 Key-Value Observing 学习笔记
Leavea reply
Key-Value Coding 解决什么问题?
Objective-C 有点像脚本语言, 有很多动态特性. 例如可以从一个字符串生成相应的类:NSClassFromString(@"ClassName"),
将一个字符串转化为消息 (类的方法):
NSSelectorFromString(@"setValue:ForKey:")等等. Key-Value Coding
(KVC) 也是类似的一种动态特性, 能够根据字符串直接操作对象的属性, 用起来像操作一个字典一样操作对象, 当然实际作用远大于此.
文档里的例子:
不用 KVC 时的代码是这样:
- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row { ChildObject *child = [childrenArray objectAtIndex:row]; if ([[column identifier] isEqualToString:@"name"]) { return [child name]; } if ([[column identifier] isEqualToString:@"age"]) { return [child age]; } if ([[column identifier] isEqualToString:@"favoriteColor"]) { return [child favoriteColor]; } // And so on. }
用了 KVC 以后代码可以精简成这样:
- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row { ChildObject *child = [childrenArray objectAtIndex:row]; return [child valueForKey:[column identifier]]; }
使用 KVC 读写对象的属性
读取属性可以使用 [someObject valueForKey:@"<key>"], 前提是对象定义了名为
<key>或者
_<key>的成员变量,
或者声明了
-<key>方法. 写入时使用
[someObject setValue:v forKey:@"<key>"], 相应的对象需要声明了
-set<Key>:方法. 所以, 一般只要把某个属性定义为
@property, 这个属性就符合了 KVC 的要求.
用
-setValuesForKeysWithDictionary:可以批量更新一批属性的值, 类似 python 中
dict.update()方法.
如果对象中不存在
<key>这个属性, 那么读取时会调用
someObject的
-valueForUndefinedKey:方法,
默认实现会抛出
NSUndefinedKeyException异常. 写入不存在的属性时会相应的调用
-setValue:forUndefinedKey:方法,
默认抛出相同的异常.
KVC 的一系列方法是在 Foundation 框架中以 NSObject 的一个 catalog 的形式定义的 (NSKeyValueCoding.h), 所以使用时无需额外声明什么, 所有对象都支持.
KeyPath 是什么
除了 -valueForKey:和
-setValue:forKey:,
还有
-valueForKeyPath:和
-setValue:forKeyPath:方法.
KeyPath 的作用是, 假如 someObject 的 child 成员变量也是一个类, child 有一个 int 型的成员变量 val, 那么可以如下设置 val 的值:
[someObject setValue:@2 forKeyPath:@"child.val"];
效果和
someObject.child.val = 2;
是一样的.
如果 object 是一个字典, 那么还可以通过 KeyPath 访问字典中的键值, 甚至嵌套多层. 例如下面这个类:
@interface Test : NSObject @property (strong, nonatomic) NSMutableDictionary *dict; @end @implementation Test - (id)init { self = [super init]; if (self) { self.dict = [[NSMutableDictionary alloc] init]; } return self; } @end
执行下面这些代码:
Test *t = [[Test alloc] init]; [t setValue:@1 forKeyPath:@"dict.a"]; NSLog(@"%@", [t valueForKeyPath:@"dict.a"]); [t setValue:[[NSMutableDictionary alloc] init] forKeyPath:@"dict.b"]; [t setValue:@2 forKeyPath:@"dict.b.c"]; NSLog(@"%@", [t valueForKeyPath:@"dict.b.c"]); NSLog(@"%@", [t valueForKeyPath:@"dict"]);
会得到这些输出:
2012-08-15 22:21:26.254 TestKvcKvo[768:c07] 1 2012-08-15 22:21:26.255 TestKvcKvo[768:c07] 2 2012-08-15 22:21:26.255 TestKvcKvo[768:c07] { a = 1; b = { c = 2; }; }
再也不用写嵌套好几层的
[[xx objectForKey:@"yy"] objectForKey:"zz"]了.
如果属性是数组或集合?
简单的方法是通过 @property 或者 getter/setter 把容器整个暴露出来, 当然更好的实践是提供额外的方法来封装. 如果想用到 KVC 提供的额外特性, 封装方法需要按下面的规范来命名. 基本上每个方法都有 NSArray 或者 NSSet 中的相应方法.对数组形式的属性:
读取操作:
-countOf<Key>: 必须实现;
-objectIn<Key>AtIndex:或者
-<key>AtIndexes:至少实现一个;
可选的
-get<Key>:range:, 类似 NSArray 的
-getObjects:range:.
写入操作:
-insertObject:in<Key>AtIndex:或
-insert<Key>:atIndexes:至少实现一个;
-removeObjectFrom<Key>AtIndex:或
-remove<Key>AtIndexes:至少实现一个;
可选的
-replaceObjectIn<Key>AtIndex:withObject:或者
-replace<Key>AtIndexes:with<Key>:.
对集合形式的属性:
读取操作:
-countOf<Key>: 必须实现;
-enumeratorOf<Key>: 必须实现, 对应 NSSet 的方法
-objectEnumerator;
-memberOf<Key>: 必须实现, 对应 NSSet 的方法
-member:.
写入操作:
-add<Key>Object:或
-add<Key>:至少实现一个;
-remove<Key>Object:或
-remove<Key>:至少实现一个;
可选的
-intersect<Key>:, 对应 NSMutableSet 的
-intersetSet:.
这么做的好处?
可以使用 Key-Value Observing, 后面有详细说明;
KeyPath 里面可以使用一些额外的操作. 例如
someObject.items是一个数组形式的属性, 那么使用
NSNumber *avg = [someObject valueForKeyPath:@"@avg.items"];可以得到所有 items 的平均值. 类似的操作还有 @count, @max, @min, @sum, 完整的列表见链接.
注意对象间比较大小的操作是通过
-compare:方法进行的, 因此只要实现了这个方法, 就可以对自定义对象使用上面这些操作. 用起来有点像 STL, 不过这些这些操作都是定死的,
没法自己扩展.
Key-Value Observing 解决什么问题?
相当于在 Foundation 框架内提供了一个观察者模式的解决方案, 所有满足 KVC 条件的对象都可以使用. 当对象的某个属性发生变化时, 会向相关的观察者发送通知, 告知哪个属性变化了, 从什么值变成什么. 甚至可以在值发生变化之前得到通知.假如一个类的声明如下:
@interface Test : NSObject @property (assign, nonatomic) int value; @end @implementation Test @end
另一个对象 (假设是 AppDelegate) 想要观察某个 Test 类实例的 value 属性的变化, 首先需要将自己注册成观察者:
Test *t = [[Test alloc] init]; [t addObserver:self forKeyPath:@"value" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil ];
观察者自己需要实现
- (void)observeValueForKeyPath:ofObject:change:context:这个方法. 当观察的属性发生变化时,
这个方法会被自动调用:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"observer: keyPath='%@' change: %@", keyPath, change); }
那么只要 t.value 发生变化, 例如
t.value = 1;就会得到这样的输出:
2012-08-16 09:03:44.303 TestKvcKvo[13610:c07] observer: keyPath='value' change: { kind = 1; new = 1; old = 0; }
对数组或集合进行观察
使用时跟普通属性没什么区别, change字典同样会告知发生了什么变化, 是添加, 删除还是替换, 受影响的下标是哪些. 例如上面的 Test 类新增了一个
@property (nonatomic, strong) NSMutableArray *arr;, 并按照 KVC 的要求实现了相应的封装方法. 当下面的变化发生时:
[t insertObject:@1 inArrAtIndex:0]; [t insertObject:@2 inArrAtIndex:1]; [t replaceObjectInArrAtIndex:0 withObject:@3]; [t removeObjectFromArrAtIndex:0]; [t.arr insertObject:@100 atIndex:0]; // 不会触发 KVO
会得到以下输出 (观察函数同上):
2012-08-16 09:03:44.300 TestKvcKvo[13610:c07] observer: keyPath='arr' change: { indexes = "<NSIndexSet: 0x6e659e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = ( 1 ); } 2012-08-16 09:03:44.301 TestKvcKvo[13610:c07] observer: keyPath='arr' change: { indexes = "<NSIndexSet: 0x6a61980>[number of indexes: 1 (in 1 ranges), indexes: (1)]"; kind = 2; new = ( 2 ); } 2012-08-16 09:03:44.302 TestKvcKvo[13610:c07] observer: keyPath='arr' change: { indexes = "<NSIndexSet: 0x6e659e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 4; new = ( 3 ); old = ( 1 ); } 2012-08-16 09:03:44.302 TestKvcKvo[13610:c07] observer: keyPath='arr' change: { indexes = "<NSIndexSet: 0x6e659e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 3; old = ( 3 ); }
kind 的值可以取以下 4 种:
enum { NSKeyValueChangeSetting = 1, // 普通赋值 NSKeyValueChangeInsertion = 2, // 容器插入 NSKeyValueChangeRemoval = 3, // 容器删除 NSKeyValueChangeReplacement = 4 // 容器替换 }; typedef NSUInteger NSKeyValueChange;
当然使用的时候无论是
change字典中的键名还是键值都必须用常量来检查. 官方文档里还有很多细节, 比如当一个属性是由其他属性计算得到时怎么做 KVO,
以及内部的实现原理等等.
相关文章推荐
- Objective-c KVC and KVO and 通知
- NSOperation and KVO/KVC coding
- iOS开发系列--Objective-C之KVC、KVO
- iOS开发系列--Objective-C之KVC、KVO
- Objective c KVO/KVC做了简单的…
- What are KVO and KVC?
- kvo_and_kvc
- iOS基础 - KVC and KVO
- 转载大神IOS开发系列【6】--Objective-C之KVC、KVO
- 『IOS』IOS开发系列--Objective-C之KVC、KVO
- KVO and KVC
- Objective-C基础之KVC,KVO
- Objective-C KVC&KVO
- IOS开发系列--Objective-C之KVC、KVO
- Objective-C中的KVC与KVO(上)
- IOS开发系列--Objective-C之KVC、KVO
- [objective-c]关于KVC--KVO--KVB
- Objective-C中的KVC和KVO
- Objective-C_语言_KVO和KVC
- 移动开发(IOS) – Objective-C-10-KVC、谓词、KVO与通知