KVC & KVO 小結

KVC

什么是KVC?

KVC(Key-value coding)是一種通過字符串去識別并間接存取(access)對象屬性的機制, 該機制區(qū)別于直接通過存取方法(accessor)和實例變量去訪問. 本質上, KVC定義了實現存取方法的模式與方法簽名.

KVC可用來訪問三種不同類型的對象值: attribute, 一對一關系, 一對多關系.

KVC方法

讀:

  • -valueForKey:
  • -valueForKeyPath:
  • -dicitionaryWithValuesForKeys:

對應key的值不存在時將發(fā)送消息valueForUndefinedKey:給自己, 該方法默認實現拋出NSUndefinedKeyException. 可自行重寫該方法.

寫:

  • -setValue:ForKey:
  • -setValue:ForKeyPath:
  • -setValuesForKeysWithDicitonary:

若指定的key不存在調用者將被發(fā)送消息setValue:forUndefinedKey:, 同樣該方法默認拋出NSUndefinedKeyException.

若把nil賦給一個非對象類型的屬性, 調用者被發(fā)送setNilValueForKey:消息, 該方法默認拋出NSInvalidArgumentException. 自己可重寫該方法來實現正確的賦值. 例如:

// MyModel.h
@interface MyModel : NSObject
@property (nonatomic, assign) BOOL hidden;
@property (nonatomic, assign, readonly) NSInteger num;
@end


// MyModel.m
@implementation MyModel
- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"num"]) {
        [self setValue:@0 forKey:@"num"];
    } else if ([key isEqualToString:@"hidden"]) {
        [self setValue:@NO forKey:@"hidden"];
    } else {
        [super setNilValueForKey:key];
    }
}
@end

KVC與點語法訪問方法

兩種方法可同時并存使用.
如下例所示, 定義了一個類:

@interface MyClass
@property NSString *stringProperty;
@property NSInteger integerProperty;
@property MyClass *linkedInstance;
@end

用KVC訪問屬性如下:

MyClass *myInstance = [[MyClass alloc] init];
NSString *string = [myInstance valueForKey:@"stringProperty"];
[myInstance setValue:@2 forKey:@"integerProperty"];

以下兩種方式結果是一樣的:

MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
myInstance.linkedInstance.integerProperty = 2;
MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
[myInstance setValue:@2 forKeyPath:@"linkedInstance.integerProperty"];

KVC兼容(Key-value coding compliant)

KVC兼容是對于類中某個特定屬性(property)來說的, 所謂KVC兼容實際指的是某個屬性可通過-valueForKey:, -setValue:forKey:KVC方法去訪問屬性.

在Objective-C 2.0里的@property實質上也就是一對setter, getter訪問器方法加一個實例變量.

先看個例子:
//MyModel.m

@implementation

...
{
    NSObject *_myObj;
}

- (NSObject *)myObj {
    if (!_myObj) {
        _myObj = [[NSObject alloc] init];
    }
    return _myObj;
}

- (void)setMyObj:(NSObject *)obj {
    _myObj = obj;
}

@end

調用時,

NSLog(@"MyModel myObj: %@", model.myObj);
NSObject *obj = [[NSObject alloc] init];
NSLog(@"auto obj: %@", obj);
[model setValue:obj forKey:@"myObj"];
NSLog(@"MyModel myObj: %@", model.myObj);

打印出來:

2016-06-30 15:19:26.991 TestKVC[19217:20399799] MyModel myObj: <NSObject: 0x7fea43424fe0>
2016-06-30 15:19:26.991 TestKVC[19217:20399799] auto obj: <NSObject: 0x7fea45813d90>
2016-06-30 15:19:26.991 TestKVC[19217:20399799] MyModel myObj: <NSObject: 0x7fea45813d90>

以上, 對于myObj這個屬性來說, 它就是KVC兼容的.

其實, 沒有實例變量也是可以的.

// MyModel.m
...
- (NSObject *)noSuchObj {
    NSLog(@"getter method");
    return nil;
}

- (void)setNoSuchObj:(NSObject *)obj {
    NSLog(@"setter method");
}

調用時,

//invoke getter accessor
[model valueForKey:@"noSuchObj"];
//invoke setter accessor
[model setValue:@"nothing" forKey:@"noSuchObj"];
//same as the above one for dot syntax
model.noSuchObj = @"nothing";

打印出來是,

2016-06-30 15:19:26.992 TestKVC[19217:20399799] getter method
2016-06-30 15:19:31.439 TestKVC[19217:20399799] setter method
2016-06-30 15:19:38.882 TestKVC[19217:20399799] setter method

實際上-valueForKey:, -setValue:forKey:之類的KVC方法在運行時會按照一定的順序去調用遵循特定方法簽名的訪問器方法或直接訪問實例變量.

例如-setValue:forKey:這個方法的實現大概是這樣的:

  • 查看調用對象所屬類是否實現了-set<Key>:這樣的訪問器方法, 有則調用;
  • 若沒有, 調用者類方法-accessInstanceVariablesDirectly放回YES, 然后依次搜索看是否存在命名方式為 _<key>, _is<Key>, <key>, is<Key>這樣的實例變量, 有則直接賦值于它.
  • 若遵循這種命名形式的訪問器方法和實例變量都無, 則調用setValue:forUndefinedKey:方法.

一對多關系屬性的KVC, 集合代理

先看個例子:

//MyModel.m

...

static int32_t const primes[] = {
    2, 101, 233, 383, 3, 103, 239, 389, 5, 107, 241, 397, 7, 109,
    251, 401, 11, 113, 257, 409, 13, 127, 263, 419, 17, 131, 269,
    421, 19, 137, 271, 431, 23, 139, 277, 433, 29, 149, 281, 439,
    31, 151, 283, 443, 37, 157, 293, 449, 41, 163, 307, 457, 43,
    167, 311, 461, 47, 173, 313, 463, 53, 179, 317, 467, 59, 181,
    331, 479, 61, 191, 337, 487, 67, 193, 347, 491, 71, 197, 349,
    499, 73, 199, 353, 503, 79, 211, 359, 509, 83, 223, 367, 521,
    89, 227, 373, 523, 97, 229, 379, 541, 547, 701, 877, 1049,
    557, 709, 881, 1051, 563, 719, 883, 1061, 569, 727, 887,
    1063, 571, 733, 907, 1069, 577, 739, 911, 1087, 587, 743,
    919, 1091, 593, 751, 929, 1093, 599, 757, 937, 1097, 601,
    761, 941, 1103, 607, 769, 947, 1109, 613, 773, 953, 1117,
    617, 787, 967, 1123, 619, 797, 971, 1129, 631, 809, 977,
    1151, 641, 811, 983, 1153, 643, 821, 991, 1163, 647, 823,
    997, 1171, 653, 827, 1009, 1181, 659, 829, 1013, 1187, 661,
    839, 1019, 1193, 673, 853, 1021, 1201, 677, 857, 1031,
    1213, 683, 859, 1033, 1217, 691, 863, 1039, 1223, 1229,
};

- (NSUInteger)countOfPrimes;
{
    return (sizeof(primes) / sizeof(*primes));
}

- (id)objectInPrimesAtIndex:(NSUInteger)idx;
{
    NSParameterAssert(idx < sizeof(primes) / sizeof(*primes));
    return @(primes[idx]);
}

在上述MyModel類的實現文件里, 加上了一個裝有一堆質數的靜態(tài)數組, 以及定義了兩個方法.

瞧這兩方法是否看起來類似于NSArray的原始(primitive)方法 -count-objectAtInIndex:呢?

使用時:

//invoke collection accessors
id proxy = [model valueForKey:@"primes"];
NSLog(@"MyModel last prime: %@", [proxy lastObject]);

這里我們是把primes當做一個NSArray來使用的. 注意上面例子中并沒有聲明有primes這么一個屬性的, 也無這么一個實例變量對象. 我們可以通過valueForKey:去獲取, 那么它一定是實現KVC了.

實際上, 這里valueForKey:方法內部大概是這樣的:

  • 依次查找get<Key>, <key>, is<Key>形式的訪問器方法.
  • 找不到則查找匹配模式countOf<Key>objectIn<Key>AtIndex:(或objectsAtIndexes:)的方法. 若找到, 則返回一個集合代理對象(collection proxy object), 該對象可使用所有NSArray的方法.
  • 找不到則查找countOf<Key>, enumeratorOf<Key>, memberOf<Key>:, 同理如上, 不過對應的是NSSet, 無序集合.
  • 再找不到則查找匹配命名_<key>, _is<Key>, <key>, or is<Key>的實例變量.
  • 否則調用valueForUndefinedKey:.

上例中的proxy就是一個集合代理對象, 調用[proxy class]得知它是一個NSKeyValueArray類型.

集合代理對象里, 包括了有序與無序, 可變與不可變的不同情況, 其分別也對應于實現不同的方法以KVC兼容. 但它們機理都是類似的.

集合運算符

Operator key path format
Operator key path format
  • 簡單運算符:@avg, @count, @max, @min. @sum.
  • 對象運算符:@distinctUnionOfObjects, @unionOfObjects.
  • 數組,集合運算符:@distinctUnionOfArrays, @unionOfArrays, @distinctUnionOfSets.

上例中,

id proxy = [model valueForKey:@"primes"];
NSLog(@"MyModel primes count: %lu", [proxy count]);
NSLog(@"MyModel primes count: %@", [model valueForKeyPath:@"primes.@count"]);

打印的兩個count結果都是一樣的, 都是201.

KVO

接收一個屬性的KVO通知, 需三個條件:

  • 被觀察的屬性是KVO兼容的.(KVO Compliant)
  • 注冊觀察者:發(fā)送消息addObserver:forKeyPath:options:context:到被觀察對象.
  • 觀察著對象需實現observeValueForKeyPath:ofObject:change:context:方法.

而屬性為KVO兼容的, 亦需滿足三個條件:

  • 它是KVC兼容的;
  • 所在類會為該屬性發(fā)送KVO通知;
  • 依賴鍵需注冊.

看個例子. 以下兩方法都在都一觀察者類中, self即observer.

// 觀察者類中注冊鍵值觀察

- (void)registerAsObserver {
    self.model = [[MyModel alloc] init];
    
    [self.model addObserver:self
            forKeyPath:@"num"
               options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
               context:nil];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self.model setValue:@"999" forKey:@"num"];
    });
}
// 實現處理KVO通知方法

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
    if (object == self.model) {
        if ([keyPath isEqualToString:@"num"]) {
            NSLog(@"old num:%@", change[NSKeyValueChangeOldKey]);
            NSLog(@"new num:%@", change[NSKeyValueChangeNewKey]);
        }        
    }
}

這里打印被觀察屬性值變化的新舊值.

KVO的自動與手動通知

以上例子為自動通知. 實際上蘋果系統(tǒng)framework中類的屬性都支持發(fā)送自動通知.

而手動通知可實現精細的控制通知發(fā)送. 手動通知通過重寫NSObject的automaticallyNotifiesObserversForKey:方法判斷是否自動.

用法如下例所示:

//MyModel.m
...

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"num"]) {
        return NO;
    } else {
        return [super automaticallyNotifiesObserversForKey:key];
    }
}

NO表示鍵為num的屬性為手動KVO.
重寫其setter方法:

//MyModel.m
...

- (void)setNum:(NSInteger)num {
    if (num != _num) {
        [self willChangeValueForKey:@"num"];
        _num = num;
        [self didChangeValueForKey:@"num"];
    }
}

參考資料:

Key-Value Observing Programming Guide
Key-Value Coding Programming Guide
KVC 和 KVO objc中國

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容