或者KVO,是一個非正式協(xié)議,它定義了對象之間觀察和通知狀態(tài)改變的通用機制。
基本使用
使用KVO必須要滿足的條件和一般使用步驟:
1.該對象必須支持KVC(凡是繼承自NSObject的類都支持KVC)2.作為觀察者的對象必須實現(xiàn) -(void)observeValueForKeyPath:ofObject:change:context:方法3.被觀察的對象要用- (void)addObserver:forKeyPath:options:context:方法注冊觀察者4.用完要移除。附上方法- (void)removeObserver:forKeyPath:或者- (void)removeObserver:forKeyPath:context:
關(guān)于這幾個方法里面的參數(shù),需要一個一個說明。從-(void)observeValueForKeyPath:ofObject:change:context:方法開始。
//keyPath:被觀察的屬性- (void)observeValueForKeyPath:(nullableNSString*)keyPath//object:被觀察的屬性所屬的對象ofObject:(nullableid)object//change:這是一個字典,它包含了屬性被修改的一些信息。//這個字典中包含的值會根據(jù)我們在添加觀察者時(addObserver方法)設(shè)置的options參數(shù)有所變化。change:(nullableNSDictionary *)change//context:添加觀察者時的上下文信息,它可以被用作區(qū)分那些綁定同一個keypath的不同對象的觀察者。//比如說觀察一些繼承自同一個父類的子類,而這些子類都有一個相同的keyPath。context:(nullablevoid*)context;
關(guān)于change字典里面的鍵值對,系統(tǒng)提供了這些預(yù)定義的key供我們使用
NSKeyValueChangeKindKey可以用@"kind"替代,也就是change[NSKeyValueChangeKindKey]等價于change[@"kind"]NSKeyValueChangeNewKey可以用@"new"替代NSKeyValueChangeOldKey可以用@"old"替代NSKeyValueChangeIndexesKey可以用@"indexes"替代NSKeyValueChangeNotificationIsPriorKey可以用@"notificationIsPrior"替代
change字典里面會有哪些key出現(xiàn)取決于在addObserver方法中options參數(shù)的設(shè)置情況。(如果有人在看這篇文章的話建議先看下面addObserver方法參數(shù)和NSKeyValueObservingOptions的那部分內(nèi)容,然后再回來看這段,因為這里的key和options關(guān)聯(lián)很大。原諒我- -||)NewKey和OldKey很簡單,就是options設(shè)置NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld時會在change里加入的鍵值對。
NSKeyValueChangeNotificationIsPriorKey是在設(shè)置了NSKeyValueObservingOptionPrior選項后當(dāng)被觀察的值將要改變(但是還未改變)時發(fā)送的通知里會有的key,對應(yīng)的是一個布爾值。
NSKeyValueChangeKindKey對應(yīng)的value是一個枚舉值(NSKeyValueChange,就是下面這個),當(dāng)被觀察的值被設(shè)置時(setter方法調(diào)用時)KindKey對應(yīng)的值為1(NSKeyValueChangeSetting)。
如果觀測的值是一個可變數(shù)組,那么當(dāng)數(shù)組執(zhí)行插入,刪除,替換時kindKey會對應(yīng)Insertion,Removal和Replacement。
typedefNS_ENUM(NSUInteger,NSKeyValueChange) {NSKeyValueChangeSetting= 1,NSKeyValueChangeInsertion= 2,NSKeyValueChangeRemoval= 3,NSKeyValueChangeReplacement= 4,};
NSKeyValueChangeIndexesKey:當(dāng)NSKeyValueChangeKindKey對應(yīng)了2/3/4這幾個值得時候,這個key的value是一個NSIndexSet,包含了發(fā)生insert,remove,replace的對象的索引集合。如果這個時候打印一下change字典大概會看到里面這樣的一個鍵值對。
indexes="<_NSCachedIndexSet: 0x7fde0a50cd20>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
然后是- (void)addObserver:forKeyPath:options:context:方法,這個方法在調(diào)用時,觀察者和被觀察者對象的引用計數(shù)都不會增加。也就是在對象被釋放之后,如果KVO的監(jiān)聽信息依然存在的話會導(dǎo)致程序崩潰。所以在適當(dāng)?shù)臅r候要記得使用removeObserver方法將觀察者信息remove掉。
//observer:觀察者對象,也就是實現(xiàn)了observeValueForKeyPath:ofObject:change:context:方法的對象- (void)addObserver:(NSObject *)observer//keyPath:被觀察的屬性forKeyPath:(NSString *)keyPath//options:監(jiān)聽選項,這個值可以是NSKeyValueObservingOptions選項的組合//也就是可以這么寫(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)options:(NSKeyValueObservingOptions)options//context:同上面的方法context:(nullablevoid*)context;
關(guān)于NSKeyValueObservingOptions,里面一共有四個值:
//設(shè)置后會在observeValueForKeyPath方法的change字典里存入更新后的值。NSKeyValueObservingOptionNew//設(shè)置后會在observeValueForKeyPath方法的change字典里存入更新前的值,也就是原有的值。NSKeyValueObservingOptionOld//設(shè)置后會在添加觀察者的時候立即發(fā)送一次通知給觀察者,并且在注冊觀察者方法之前返回。//也就是在addObserver方法執(zhí)行之后就立即發(fā)送了一次通知。NSKeyValueObservingOptionInitial//會在值被改變之前發(fā)送一次通知,并且在change字典里多了一個叫notificationIsPrior的key,值是1。//而且change字典不會包含new(NSKeyValueChangeNewKey)這個key。//當(dāng)然值改變后的那次通知也會發(fā),也就是說會發(fā)送兩次通知。NSKeyValueObservingOptionPrior
當(dāng)觀察者不再需要監(jiān)聽屬性變化時,需要使用- (void)removeObserver:forKeyPath: 或者- (void)removeObserver:forKeyPath:context:來移除觀察者,需要注意的是如果移除了一個沒有觀察過的屬性,程序會拋出異常。也就是說如果之前觀察的是"property1",而在移除的時候keyPath參數(shù)寫的是"property2",這是就會有異常被拋出。可以使用@try/@catch來防止崩潰。
@try{[object removeObserver:observer forKeyPath:@"keyPath")];}@catch(NSException * __unused exception) {}
手動通知
默認(rèn)情況下通知會被自動發(fā)送,但有的時候我們希望可以手動的控制它。這時候需要在被觀察對象的類里面重寫+ (BOOL)automaticallyNotifiesObserversForKey:方法。例如被觀察對象有一個屬性叫"bankCodeEn",我們希望這個屬性被修改時的通知由我們手動控制,就需要在被觀察對象的類文件里面這樣寫:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString*)key{// 如果屬性為bankCodeEn則關(guān)閉自動發(fā)送通知BOOLautomatic =YES;if([key isEqualToString:@"bankCodeEn"]) {? ? ? automatic =NO;? }else{// 對于對象中其它沒有處理的屬性,我們需要調(diào)用[super automaticallyNotifiesObserversForKey:key],以避免無意中修改了父類的屬性的處理方式automatic = [superautomaticallyNotifiesObserversForKey:key];? }returnautomatic;}
然后再對"bankCodeEn"屬性的setter方法做如下處理:
- (void)setBankCodeEn:(NSString *)bankCodeEn{//當(dāng)兩次賦予的值完全相等時,沒有必要再發(fā)送通知。這個if的條件語句可以根據(jù)實際需要自行修改,或者干脆不寫。if(_bankCodeEn != bankCodeEn) {? ? ? [selfwillChangeValueForKey:@"bankCodeEn"];? ? ? _bankCodeEn = bankCodeEn;? ? ? [selfdidChangeValueForKey:@"bankCodeEn"];? }}
注意 willChangeValueForKey:和didChangeValueForKey:方法在默認(rèn)自動發(fā)送通知的情況下是由系統(tǒng)自動調(diào)用的,在手動通知時需要我們自己來調(diào)用,并且不應(yīng)該重寫這兩個方法。
注冊依賴建
有時一個屬性的改變需要依賴其他的屬性,比如一個叫"fullName"的屬性,這個屬性依賴于"firstName"和"lastName"。
//fullName的getter方法- (NSString*)fullName{return[NSStringstringWithFormat:@"%@? %@", _firstName, _lastName];}
這種情況下如果firstName發(fā)生了變化,fullName的值自然也會改變,但是由于沒有直接使用setter方法設(shè)置fullName,所以如果不做特殊設(shè)置的話KVO是不會發(fā)送通知的。
這種情況就需要使用注冊依賴建來解決。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{? NSSet *keyPaths = [superkeyPathsForValuesAffectingValueForKey:key];if([keyisEqualToString:@"fullName"]) {? ? ? keyPaths = [keyPathssetByAddingObjectsFromArray:@[@"firstName", @"lastName"]];? }returnkeyPaths;}
這樣不論firstName,lastName,fullName中的哪個值放生了變化,監(jiān)聽fullName的KVO都會被觸發(fā)。還可以使用這個方法來達(dá)到同樣的目的。
+ (NSSet*)keyPathsForValuesAffectingFullName {return[NSSetsetWithObjects:@"firstName",@"lastName",nil];}
這個方法的使用規(guī)則是+ (NSSet *)keyPathsForValuesAffecting + 屬性名(注意屬性名首字母大寫)。
屬性類型為集合的監(jiān)聽
對于集合的KVO,我們需要了解的一點是,KVO旨在觀察關(guān)系(relationship)而不是集合。對于不可變集合屬性,我們更多的是把它當(dāng)成一個整體來監(jiān)聽,而無法去監(jiān)聽集合中的某個元素的變化;對于可變集合屬性,實際上也是當(dāng)成一個整體,去監(jiān)聽它整體的變化,如添加、刪除和替換元素。
例如一個叫arr的NSArray類型屬性,我們可以使用集合代理對象(collection proxy object)來處理集合相關(guān)的操作。有下面的幾個代理方法需要實現(xiàn)
-countOf// 以下兩者二選一-objectInAtIndex:-AtIndexes:// 可選(增強性能)-get:range:
具體實現(xiàn)如下
- (NSUInteger)countOfArr{return[_arrcount];}- (id)objectInArrAtIndex:(NSUInteger)index{return[_arr objectAtIndex:index];}
當(dāng)我們使用對象的arr屬性時,通過[object valueForKey:@"arr"]來獲取該屬性,這個方法返回的代理數(shù)組對象支持所有正常的NSArray調(diào)用。換句話說,調(diào)用者并不知道返回的是一個真正的NSArray,還是一個代理的數(shù)組。
對于可變數(shù)組的操作
對于可變數(shù)組的代理對象,我們需要實現(xiàn)以下幾個方法:
// 至少實現(xiàn)一個插入方法和一個刪除方法-insertObject:inAtIndex:-removeObjectFromAtIndex:-insert:atIndexes:-removeAtIndexes:// 可選(增強性能)以下方法二選一-replaceObjectInAtIndex:withObject:-replaceAtIndexes:with:
實現(xiàn)如下
- (NSUInteger)countOfArr{return[_arr count];}- (id)objectInArrAtIndex:(NSUInteger)index{return[_arrobjectAtIndex:index];}- (void)insertObject:(id)objectinArrAtIndex:(NSUInteger)index{? [_arrinsertObject:objectatIndex:index];}- (void)removeObjectFromArrAtIndex:(NSUInteger)index{? [_arrremoveObjectAtIndex:index];}- (void)replaceObjectInArrAtIndex:(NSUInteger)indexwithObject:(id)object{? [_arrreplaceObjectAtIndex:indexwithObject:object];}
方法實現(xiàn)后,需要使用[object mutableArrayValueForKey:@"arr"]來訪問arr屬性才能或取到代理數(shù)組。在使用時訪問真正數(shù)組對象和集合代理對象差別還是很大的。
BankObject*bankInstance = [[BankObjectalloc] init];PersonObject*personInstance = [[PersonObjectalloc] init];[bankInstance addObserver:personInstance forKeyPath:@"departments"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:NULL];bankInstance.departments = [[NSMutableArrayalloc] init];[bankInstance.departments addObject:@"departments"];
這段代碼BankObject是被觀察對象,PersonObject是觀察者對象。BankObject類里面有一個叫departments的可變數(shù)組屬性。
這段代碼只會觸發(fā)一次KVO,也就是只有在給departments賦予一個初始化數(shù)組的時候KVO被觸發(fā),在給數(shù)組添加內(nèi)容的時候并沒有觸發(fā)。
BankObject*bankInstance = [[BankObjectalloc] init];PersonObject*personInstance = [[PersonObjectalloc] init];[bankInstance addObserver:personInstance forKeyPath:@"departments"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:NULL];bankInstance.departments = [[NSMutableArrayalloc] init];NSMutableArray*departments = [bankInstance mutableArrayValueForKey:@"departments"];[departments insertObject:@"departments 0"atIndex:0];
使用集合代理對象的方式會觸發(fā)兩次KVO,在給數(shù)組插入(刪除,替換)數(shù)據(jù)的時候KVO也會被觸發(fā)。
監(jiān)聽信息
對于被觀察的對象,可以使用observationInfo屬性獲取都有哪些觀察者觀察了哪些屬性。
id info = bankInstance.observationInfo;
NSLog(@"%@", [info description]);
如果像這樣獲取了一個被觀察對象的info然后打印出來,會看到這樣的結(jié)果。
(Context:0x0,Property:0x7fdc236a15c0>Context:0x0,Property:0x7fdc236a1880>)
我們可以看到observationInfo指針實際上是指向一個NSKeyValueObservationInfo對象,它包含了指定對象上的所有的監(jiān)聽信息。而每條監(jiān)聽信息而是封裝在一個NSKeyValueObservance對象中,從上面可以看到,這個對象中包含消息的觀察者、被監(jiān)聽的屬性、添加觀察者時所設(shè)置的一些選項、上下文信息等。
其他的一些小tips
1、如果重復(fù)添加注冊觀察者的方法(addObserver),比如像這樣完全一樣的兩句代碼重復(fù)兩次,那么通知也就會發(fā)送兩次,系統(tǒng)不會檢查也不會替換覆蓋。
[bankaddObserver:per1forKeyPath:NSStringFromSelector(@selector(departments))options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOldcontext:nil];[bankaddObserver:per1forKeyPath:NSStringFromSelector(@selector(departments))options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOldcontext:nil];
2、因為keyPath是字符串類型,這就導(dǎo)致寫錯的情況很容易發(fā)生,keyPath寫錯嚴(yán)重的話就會導(dǎo)致程序崩潰。所以為了避免這種情況,可以將@"property"替換成NSStringFromSelector(@selector(property)),這樣寫首先在敲屬性名的時候會有提示,而且在你把屬性名敲錯的時候由于xcode沒有在對應(yīng)的類里面找到那個被你寫錯的屬性,就會報出警告。像這樣:
[bankaddObserver:per1forKeyPath:NSStringFromSelector(@selector(departments))options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOldcontext:nil];
3、關(guān)于context可以這樣設(shè)置,一個靜態(tài)變量存著它自己的指針。這意味著它自己什么也沒有。
staticvoid* XXContext = &XXContext;
關(guān)于KVO的實現(xiàn)機制
KVO使用了OC的runtime來實現(xiàn),在第一次觀察一個對象時,runtime會創(chuàng)建一個繼承自被觀察對象的類的子類,這個子類重寫了被觀察屬性的setter方法,然后將這個對象的is a指針指向了這個新建的類。也就是說其實這個被觀察的對象在程序運行時所屬的類已經(jīng)不是之前我們自己寫的那個類了,而是系統(tǒng)創(chuàng)建的子類。