KVC
全稱Key-Value Coding(鍵值編碼)医寿,是一個(gè)基于NSKeyValueCoding協(xié)議實(shí)現(xiàn)的機(jī)制斑响,可通過key對對象的屬性進(jìn)行存取操作,而不是調(diào)用明確的存取方法与殃。
常見的API:
// setter方法
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
// getter方法
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
key和keyPath的區(qū)別:
keyPath的功能要更強(qiáng)大母赵,keyPath可以對屬性進(jìn)行深層的訪問,key則是直接找到對應(yīng)的value授药。舉個(gè)例子:
NSDictionary *dic = @{@"array":@[@"1",@"2",@"1"],
@"dictionary":@{@"name":@"Lily",@"sexy":@"female"}};
NSString *name = [dic valueForKeyPath:@"dictionary.name"];
NSString *name1 = [[dic valueForKey:@"dictionary"] valueForKey:@"name"];
NSLog(@"%@ -- %@",name,name1);; // Lily -- Lily
除此之外keyPath還有一些便捷的用法士嚎,參見那些不為人知的KVC;
KVC還可以批量賦值和取值,
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
使用KVC的setValuesForKeysWithDictionary方法可以進(jìn)行字典轉(zhuǎn)模型呜魄;
setValue:ForKey:的實(shí)現(xiàn)原理
調(diào)用setValue:ForKey:,會按照setKey:莱衩、_setKey:的順序查找方法實(shí)現(xiàn)爵嗅,找到后傳遞參數(shù),調(diào)用方法笨蚁;沒找到方法睹晒,查看+accessInstanceVariablesDirectly方法(即是否直接訪問對象成員變量)的返回值。返回NO括细,調(diào)用setValue:forUndefinedKey:并拋出異常NSUnKnownKeyException伪很;返回YES,按照_key奋单、_isKey锉试、key、isKey順序查找成員變量找到成員變量直接賦值览濒,如果沒有找到對應(yīng)的成員變量調(diào)用setValue:forUndefinedKey:并拋出異常NSUnKnownKeyException呆盖;
valueForKey:的實(shí)現(xiàn)原理
調(diào)用valueForKey:會按照getKey、key贷笛、isKey应又、_key順序查找方法,找到直接調(diào)用乏苦;如果沒有找到方法株扛,查看+accessInstanceVariablesDirectly(即是否直接訪問對象成員變量)方法的返回值。返回NO汇荐,調(diào)用valueForUndefinedKey:并拋出異常NSUnKnownKeyException席里;返回YES,則按照_key拢驾、_isKey奖磁、key、isKey順序查找成員變量繁疤,找到成員變量直接取值咖为,如果沒有找到成員變量調(diào)用valueForUndefinedKey:并拋出異常NSUnKnownKeyException;
異常處理
根據(jù)上述setValue:ForKey:和valueForKey:的實(shí)現(xiàn)原理稠腊,如果沒有找到對應(yīng)的key或keyPath躁染,會調(diào)用對應(yīng)的異常方法。異常方法的默認(rèn)實(shí)現(xiàn)是拋出異常架忌,可以重寫下面兩個(gè)方法進(jìn)行處理避免程序崩潰吞彤。
//取值時(shí)
- (id)valueForUndefinedKey:(NSString *)key;
//賦值時(shí)
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
KVO<NSKeyValueObserving>
是一個(gè)非正式協(xié)議,定義了對象之間觀察和通知狀態(tài)改變的通用機(jī)制,即當(dāng)被觀察對象A的某個(gè)屬性發(fā)生改變時(shí)饰恕,觀察者B會收到通知挠羔,并作出響應(yīng)。
KVO使用的條件和步驟:
1.被觀察對象必須支持KVC(繼承自NSObject的類都支持KVC)埋嵌,變更屬性值時(shí)執(zhí)行了setter方法破加、或者使用了KVC賦值,才會觸發(fā)KVO機(jī)制
2.觀察者必須實(shí)現(xiàn)-(void)observeValueForKeyPath:ofObject:change:context: 方法
3.被觀察者對象要用- (void)addObserver:forKeyPath:options:context:方法注冊觀察者
4.用完要移除觀察者- (void)removeObserver:forKeyPath: 或者- (void)removeObserver:forKeyPath:context:
方法參數(shù)說明:
1雹嗦、- (void)addObserver: forKeyPath:options: context:
/*
observer:觀察者對象
keyPath:被觀察的屬性
options:監(jiān)聽選項(xiàng)范舀,這個(gè)值可以是NSKeyValueObservingOptions選項(xiàng)的組合
context:添加觀察者時(shí)的上下文信息,它可以被用作區(qū)分那些綁定同一個(gè)keypath的不同對象的觀察者了罪。比如說觀察一些繼承自同一個(gè)父類的子類锭环,而這些子類都有一個(gè)相同的keyPath
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
NSKeyValueObservingOptions
//設(shè)置后會在observeValueForKeyPath方法的change字典里存入更新后的值。
NSKeyValueObservingOptionNew
//設(shè)置后會在observeValueForKeyPath方法的change字典里存入更新前的值泊藕,也就是原有的值辅辩。
NSKeyValueObservingOptionOld
//設(shè)置后會在添加觀察者的時(shí)候立即發(fā)送一次通知給觀察者,并且在注冊觀察者方法之前返回吱七。
//也就是在addObserver方法執(zhí)行之后就立即發(fā)送了一次通知。
NSKeyValueObservingOptionInitial
//會在值被改變之前發(fā)送一次通知鹤竭,并且在change字典里多了一個(gè)叫notificationIsPrior的key踊餐,值是1。
//而且change字典不會包含new(NSKeyValueChangeNewKey)這個(gè)key臀稚。當(dāng)然值改變后的那次通知也會發(fā)吝岭,也就是說會發(fā)送兩次通知。
NSKeyValueObservingOptionPrior
2吧寺、-(void)observeValueForKeyPath:ofObject:change:context:
/*
keyPath:被觀察的屬性
object:被觀察的對象
change:這是一個(gè)字典窜管,它包含了屬性被修改的一些信息。這個(gè)字典中包含的值會根據(jù)我們在添加觀察者時(shí)(addObserver方法)設(shè)置的options參數(shù)有所變化稚机。
context:同上面的方法
*/
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
change字典里面的鍵值對幕帆,系統(tǒng)提供了這些預(yù)定義的key供我們使用
NSKeyValueChangeKindKey 可以用@"kind"替代,也就是change[NSKeyValueChangeKindKey]等價(jià)于change[@"kind"]
NSKeyValueChangeNewKey 可以用@"new"替代
NSKeyValueChangeOldKey 可以用@"old"替代
NSKeyValueChangeIndexesKey 可以用@"indexes"替代
NSKeyValueChangeNotificationIsPriorKey 可以用@"notificationIsPrior"替代
change字典里面會有哪些key出現(xiàn)取決于在addObserver方法中options參數(shù)的設(shè)置情況赖条。
NewKey和OldKey失乾,就是options設(shè)置NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld時(shí)會在change里加入的鍵值對。
NSKeyValueChangeNotificationIsPriorKey是在設(shè)置了NSKeyValueObservingOptionPrior選項(xiàng)后當(dāng)被觀察的值將要改變(但是還未改變)時(shí)發(fā)送的通知里會有的key纬乍,對應(yīng)的是一個(gè)布爾值碱茁。
NSKeyValueChangeKindKey對應(yīng)的value是一個(gè)枚舉值,當(dāng)被觀察的值被設(shè)置時(shí)(setter方法調(diào)用時(shí))KindKey對應(yīng)的值為1(NSKeyValueChangeSetting)仿贬。如果觀測的值是一個(gè)可變數(shù)組纽竣,那么當(dāng)數(shù)組執(zhí)行插入,刪除,替換時(shí)kindKey會對應(yīng)Insertion蜓氨,Removal和Replacement聋袋。
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
KVO的實(shí)現(xiàn)原理
當(dāng)對一個(gè)實(shí)例對象的屬性添加監(jiān)聽,在程序運(yùn)行時(shí)语盈,會動態(tài)的創(chuàng)建一個(gè)類NSKVONotifying_XXX繼承自該實(shí)例對象的類舱馅,并修改該實(shí)例對象的isa指針指向NSKVONotifying_XXX類。新創(chuàng)建的類對應(yīng)被觀察屬性的setter方法實(shí)現(xiàn)是_NSSetXXXValueAndNotify函數(shù)刀荒,_NSSetXXXValueAndNotify內(nèi)部實(shí)現(xiàn)是:
[self willChangeValueForKey:XXX];
// 調(diào)用原來的setter實(shí)現(xiàn)
[self didChangeValueForKey:XXX];
didChangeValueForKey:內(nèi)部會調(diào)用observerobserveValueForKeyPath:ofObject:change:context:方法代嗤;
NSKVONotifying_XXX類會包含以下方法:
- (void)setXXX:(int)age;//內(nèi)部調(diào)用_NSSetXXXValueAndNotify
- (Class)class;//會返回實(shí)例對象的類對象,為了屏蔽內(nèi)部實(shí)現(xiàn)缠借,隱藏NSKVONotifying_XXX類的存在干毅;
- (BOOL)_isKVOA; //表示是KVO對象
- (void)dealloc; // 說明添加KVO的對象,在銷毀時(shí)需要做一些處理工作泼返;
手動通知
默認(rèn)情況下通知會被自動發(fā)送硝逢,但我們也可以手動觸發(fā)。這時(shí)候需要在被觀察對象的類里面重寫+ (BOOL)automaticallyNotifiesObserversForKey:方法绅喉。例如被觀察對象有一個(gè)屬性叫"count"渠鸽,我們希望這個(gè)屬性被修改時(shí)的通知由我們手動控制,就需要在被觀察對象的類文件里面這樣寫:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
// 如果屬性為count則關(guān)閉自動發(fā)送通知
BOOL automatic = YES;
if ([key isEqualToString:@"count"]) {
automatic = NO;
} else {
// 對于對象中其它沒有處理的屬性柴罐,我們需要調(diào)用[super automaticallyNotifiesObserversForKey:key]徽缚,以避免無意中修改了父類的屬性的處理方式
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
然后再對"count"屬性的setter方法做如下處理:
- (void)setCount:(NSString *) count{
//當(dāng)兩次賦予的值完全相等時(shí)凿试,沒有必要再發(fā)送通知似芝。這個(gè)if的條件語句可以根據(jù)實(shí)際需要自行修改党瓮,或者干脆不寫寞奸。
if (_ count != count) {
[self willChangeValueForKey:@"count"];
_count = count;
[self didChangeValueForKey:@"count"];
}
}
注意 willChangeValueForKey:和didChangeValueForKey:方法在默認(rèn)自動發(fā)送通知的情況下是由系統(tǒng)自動調(diào)用的,在手動通知時(shí)需要我們自己來調(diào)用呻率,并且不應(yīng)該重寫這兩個(gè)方法礼仗。
注冊依賴建
有時(shí)一個(gè)屬性的改變需要依賴其他的屬性元践,比如一個(gè)叫"fullName"的屬性,這個(gè)屬性依賴于"firstName"和"lastName"沪羔。
//fullName的getter方法
- (NSString *)fullName{
return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}
這種情況下如果firstName發(fā)生了變化蔫饰,fullName的值自然也會改變鹃唯,但是由于沒有直接使用setter方法設(shè)置fullName,所以如果不做特殊設(shè)置的話KVO是不會發(fā)送通知的蚪拦。
這種情況就需要使用注冊依賴建來解決。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"firstName", @"lastName"]];
}
return keyPaths;
}
這樣不論firstName盛嘿,lastName括袒,fullName中的哪個(gè)值發(fā)生了變化,監(jiān)聽fullName的KVO都會被觸發(fā)类垦。
還可以使用下面這個(gè)方法來達(dá)到同樣的目的。這個(gè)方法的使用規(guī)則是+ (NSSet *)keyPathsForValuesAffecting + 屬性名(注意屬性名首字母大寫)糕伐。
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}