Objective-c KVO and KVC


轉載自: 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, 以及內部的實現原理等等.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章