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