或者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:
關于這幾個方法里面的參數(shù),需要一個一個說明溢十。從-(void)observeValueForKeyPath:ofObject:change:context:方法開始。
//keyPath:被觀察的屬性- (void)observeValueForKeyPath:(nullableNSString*)keyPath//object:被觀察的屬性所屬的對象ofObject:(nullableid)object//change:這是一個字典作瞄,它包含了屬性被修改的一些信息。//這個字典中包含的值會根據(jù)我們在添加觀察者時(addObserver方法)設置的options參數(shù)有所變化危纫。change:(nullableNSDictionary *)change//context:添加觀察者時的上下文信息宗挥,它可以被用作區(qū)分那些綁定同一個keypath的不同對象的觀察者。//比如說觀察一些繼承自同一個父類的子類种蝶,而這些子類都有一個相同的keyPath契耿。context:(nullablevoid*)context;
關于change字典里面的鍵值對,系統(tǒng)提供了這些預定義的key供我們使用
NSKeyValueChangeKindKey可以用@"kind"替代螃征,也就是change[NSKeyValueChangeKindKey]等價于change[@"kind"]NSKeyValueChangeNewKey可以用@"new"替代NSKeyValueChangeOldKey可以用@"old"替代NSKeyValueChangeIndexesKey可以用@"indexes"替代NSKeyValueChangeNotificationIsPriorKey可以用@"notificationIsPrior"替代
change字典里面會有哪些key出現(xiàn)取決于在addObserver方法中options參數(shù)的設置情況搪桂。(如果有人在看這篇文章的話建議先看下面addObserver方法參數(shù)和NSKeyValueObservingOptions的那部分內容,然后再回來看這段盯滚,因為這里的key和options關聯(lián)很大踢械。原諒我- -||)NewKey和OldKey很簡單,就是options設置NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld時會在change里加入的鍵值對魄藕。
NSKeyValueChangeNotificationIsPriorKey是在設置了NSKeyValueObservingOptionPrior選項后當被觀察的值將要改變(但是還未改變)時發(fā)送的通知里會有的key内列,對應的是一個布爾值。
NSKeyValueChangeKindKey對應的value是一個枚舉值(NSKeyValueChange背率,就是下面這個)话瞧,當被觀察的值被設置時(setter方法調用時)KindKey對應的值為1(NSKeyValueChangeSetting)嫩与。
如果觀測的值是一個可變數(shù)組,那么當數(shù)組執(zhí)行插入交排,刪除划滋,替換時kindKey會對應Insertion,Removal和Replacement埃篓。
typedefNS_ENUM(NSUInteger,NSKeyValueChange) {NSKeyValueChangeSetting= 1,NSKeyValueChangeInsertion= 2,NSKeyValueChangeRemoval= 3,NSKeyValueChangeReplacement= 4,};
NSKeyValueChangeIndexesKey:當NSKeyValueChangeKindKey對應了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:方法塞椎,這個方法在調用時,觀察者和被觀察者對象的引用計數(shù)都不會增加睛低。也就是在對象被釋放之后案狠,如果KVO的監(jiān)聽信息依然存在的話會導致程序崩潰。所以在適當?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;
關于NSKeyValueObservingOptions罩抗,里面一共有四個值:
//設置后會在observeValueForKeyPath方法的change字典里存入更新后的值拉庵。NSKeyValueObservingOptionNew//設置后會在observeValueForKeyPath方法的change字典里存入更新前的值,也就是原有的值套蒂。NSKeyValueObservingOptionOld//設置后會在添加觀察者的時候立即發(fā)送一次通知給觀察者钞支,并且在注冊觀察者方法之前返回。//也就是在addObserver方法執(zhí)行之后就立即發(fā)送了一次通知操刀。NSKeyValueObservingOptionInitial//會在值被改變之前發(fā)送一次通知烁挟,并且在change字典里多了一個叫notificationIsPrior的key,值是1骨坑。//而且change字典不會包含new(NSKeyValueChangeNewKey)這個key撼嗓。//當然值改變后的那次通知也會發(fā),也就是說會發(fā)送兩次通知欢唾。NSKeyValueObservingOptionPrior
當觀察者不再需要監(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) {}
手動通知
默認情況下通知會被自動發(fā)送大州,但有的時候我們希望可以手動的控制它续语。這時候需要在被觀察對象的類里面重寫+ (BOOL)automaticallyNotifiesObserversForKey:方法。例如被觀察對象有一個屬性叫"bankCodeEn"厦画,我們希望這個屬性被修改時的通知由我們手動控制疮茄,就需要在被觀察對象的類文件里面這樣寫:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString*)key{// 如果屬性為bankCodeEn則關閉自動發(fā)送通知BOOLautomatic =YES;if([key isEqualToString:@"bankCodeEn"]) {? ? ? automatic =NO;? }else{// 對于對象中其它沒有處理的屬性,我們需要調用[super automaticallyNotifiesObserversForKey:key]根暑,以避免無意中修改了父類的屬性的處理方式automatic = [superautomaticallyNotifiesObserversForKey:key];? }returnautomatic;}
然后再對"bankCodeEn"屬性的setter方法做如下處理:
- (void)setBankCodeEn:(NSString *)bankCodeEn{//當兩次賦予的值完全相等時力试,沒有必要再發(fā)送通知。這個if的條件語句可以根據(jù)實際需要自行修改排嫌,或者干脆不寫畸裳。if(_bankCodeEn != bankCodeEn) {? ? ? [selfwillChangeValueForKey:@"bankCodeEn"];? ? ? _bankCodeEn = bankCodeEn;? ? ? [selfdidChangeValueForKey:@"bankCodeEn"];? }}
注意 willChangeValueForKey:和didChangeValueForKey:方法在默認自動發(fā)送通知的情況下是由系統(tǒng)自動調用的,在手動通知時需要我們自己來調用淳地,并且不應該重寫這兩個方法怖糊。
注冊依賴建
有時一個屬性的改變需要依賴其他的屬性,比如一個叫"fullName"的屬性颇象,這個屬性依賴于"firstName"和"lastName"伍伤。
//fullName的getter方法- (NSString*)fullName{return[NSStringstringWithFormat:@"%@? %@", _firstName, _lastName];}
這種情況下如果firstName發(fā)生了變化扰魂,fullName的值自然也會改變劝评,但是由于沒有直接使用setter方法設置fullName简肴,所以如果不做特殊設置的話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ā)置济。還可以使用這個方法來達到同樣的目的浙于。
+ (NSSet*)keyPathsForValuesAffectingFullName {return[NSSetsetWithObjects:@"firstName",@"lastName",nil];}
這個方法的使用規(guī)則是+ (NSSet *)keyPathsForValuesAffecting + 屬性名(注意屬性名首字母大寫)。
屬性類型為集合的監(jiān)聽
對于集合的KVO整慎,我們需要了解的一點是,KVO旨在觀察關系(relationship)而不是集合拧揽。對于不可變集合屬性淤袜,我們更多的是把它當成一個整體來監(jiān)聽,而無法去監(jiān)聽集合中的某個元素的變化烦周;對于可變集合屬性,實際上也是當成一個整體夭委,去監(jiān)聽它整體的變化株灸,如添加慌烧、刪除和替換元素杏死。
例如一個叫arr的NSArray類型屬性淑翼,我們可以使用集合代理對象(collection proxy object)來處理集合相關的操作冯丙。有下面的幾個代理方法需要實現(xiàn)
-countOf// 以下兩者二選一-objectInAtIndex:-AtIndexes:// 可選(增強性能)-get:range:
具體實現(xiàn)如下
- (NSUInteger)countOfArr{return[_arrcount];}- (id)objectInArrAtIndex:(NSUInteger)index{return[_arr objectAtIndex:index];}
當我們使用對象的arr屬性時,通過[object valueForKey:@"arr"]來獲取該屬性船殉,這個方法返回的代理數(shù)組對象支持所有正常的NSArray調用利虫。換句話說,調用者并不知道返回的是一個真正的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ù)組添加內容的時候并沒有觸發(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然后打印出來压储,會看到這樣的結果。
(Context:0x0,Property:0x7fdc236a15c0>Context:0x0,Property:0x7fdc236a1880>)
我們可以看到observationInfo指針實際上是指向一個NSKeyValueObservationInfo對象源譬,它包含了指定對象上的所有的監(jiān)聽信息渠脉。而每條監(jiān)聽信息而是封裝在一個NSKeyValueObservance對象中,從上面可以看到瓶佳,這個對象中包含消息的觀察者芋膘、被監(jiān)聽的屬性、添加觀察者時所設置的一些選項霸饲、上下文信息等为朋。
其他的一些小tips
1、如果重復添加注冊觀察者的方法(addObserver)厚脉,比如像這樣完全一樣的兩句代碼重復兩次习寸,那么通知也就會發(fā)送兩次,系統(tǒng)不會檢查也不會替換覆蓋傻工。
[bankaddObserver:per1forKeyPath:NSStringFromSelector(@selector(departments))options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOldcontext:nil];[bankaddObserver:per1forKeyPath:NSStringFromSelector(@selector(departments))options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOldcontext:nil];
2霞溪、因為keyPath是字符串類型,這就導致寫錯的情況很容易發(fā)生中捆,keyPath寫錯嚴重的話就會導致程序崩潰鸯匹。所以為了避免這種情況,可以將@"property"替換成NSStringFromSelector(@selector(property))泄伪,這樣寫首先在敲屬性名的時候會有提示殴蓬,而且在你把屬性名敲錯的時候由于xcode沒有在對應的類里面找到那個被你寫錯的屬性,就會報出警告蟋滴。像這樣:
[bankaddObserver:per1forKeyPath:NSStringFromSelector(@selector(departments))options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOldcontext:nil];
3染厅、關于context可以這樣設置,一個靜態(tài)變量存著它自己的指針津函。這意味著它自己什么也沒有肖粮。
staticvoid* XXContext = &XXContext;
關于KVO的實現(xiàn)機制
KVO使用了OC的runtime來實現(xiàn),在第一次觀察一個對象時尔苦,runtime會創(chuàng)建一個繼承自被觀察對象的類的子類涩馆,這個子類重寫了被觀察屬性的setter方法行施,然后將這個對象的is a指針指向了這個新建的類。也就是說其實這個被觀察的對象在程序運行時所屬的類已經不是之前我們自己寫的那個類了凌净,而是系統(tǒng)創(chuàng)建的子類。