轉載自: http://zhangbin.cc/archives/1839
Objective-C Key-Value Coding 和 Key-Value Observing 學習筆記
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,
以及內部的實現原理等等.