KVO in iOS

復習了一些iOS里大神寫的KVO官方文檔翻譯和其他的博客,記錄下來一些方便自己以后回來看。

<NSKeyValueObserving>或者KVO,是一個非正式協議,它定義了對象之間觀察和通知狀態改變的通用機制。

基本使用

使用KVO必須要滿足的條件和一般使用步驟:

1.該對象必須支持KVC(凡是繼承自NSObject的類都支持KVC)
2.作為觀察者的對象必須實現 -(void)observeValueForKeyPath:ofObject:change:context: 方法
3.被觀察的對象要用- (void)addObserver:forKeyPath:options:context:方法注冊觀察者
4.用完要移除。附上方法- (void)removeObserver:forKeyPath: 或者- (void)removeObserver:forKeyPath:context:

關于這幾個方法里面的參數,需要一個一個說明。從-(void)observeValueForKeyPath:ofObject:change:context:方法開始。

 //keyPath:被觀察的屬性
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
//object:被觀察的屬性所屬的對象
ofObject:(nullable id)object 
//change:這是一個字典,它包含了屬性被修改的一些信息。
//這個字典中包含的值會根據我們在添加觀察者時(addObserver方法)設置的options參數有所變化。
change:(nullable NSDictionary<NSString*, id> *)change
//context:添加觀察者時的上下文信息,它可以被用作區分那些綁定同一個keypath的不同對象的觀察者。
//比如說觀察一些繼承自同一個父類的子類,而這些子類都有一個相同的keyPath。
context:(nullable void *)context;

關于change字典里面的鍵值對,系統提供了這些預定義的key供我們使用

NSKeyValueChangeKindKey 可以用@"kind"替代,也就是change[NSKeyValueChangeKindKey]等價于change[@"kind"]
NSKeyValueChangeNewKey 可以用@"new"替代
NSKeyValueChangeOldKey 可以用@"old"替代
NSKeyValueChangeIndexesKey 可以用@"indexes"替代
NSKeyValueChangeNotificationIsPriorKey 可以用@"notificationIsPrior"替代

change字典里面會有哪些key出現取決于在addObserver方法中options參數的設置情況。(如果有人在看這篇文章的話建議先看下面addObserver方法參數和NSKeyValueObservingOptions的那部分內容,然后再回來看這段,因為這里的key和options關聯很大。原諒我- -||)NewKey和OldKey很簡單,就是options設置NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld時會在change里加入的鍵值對。

NSKeyValueChangeNotificationIsPriorKey是在設置了NSKeyValueObservingOptionPrior選項后當被觀察的值將要改變(但是還未改變)時發送的通知里會有的key,對應的是一個布爾值。

NSKeyValueChangeKindKey對應的value是一個枚舉值(NSKeyValueChange,就是下面這個),當被觀察的值被設置時(setter方法調用時)KindKey對應的值為1(NSKeyValueChangeSetting)。
如果觀測的值是一個可變數組,那么當數組執行插入,刪除,替換時kindKey會對應Insertion,Removal和Replacement。

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};

NSKeyValueChangeIndexesKey:當NSKeyValueChangeKindKey對應了2/3/4這幾個值得時候,這個key的value是一個NSIndexSet,包含了發生insert,remove,replace的對象的索引集合。如果這個時候打印一下change字典大概會看到里面這樣的一個鍵值對。

indexes = "<_NSCachedIndexSet: 0x7fde0a50cd20>[number of indexes: 1 (in 1 ranges), indexes: (0)]";

然后是- (void)addObserver:forKeyPath:options:context:方法,這個方法在調用時,觀察者和被觀察者對象的引用計數都不會增加。也就是在對象被釋放之后,如果KVO的監聽信息依然存在的話會導致程序崩潰。所以在適當的時候要記得使用removeObserver方法將觀察者信息remove掉。

//observer:觀察者對象,也就是實現了observeValueForKeyPath:ofObject:change:context:方法的對象
- (void)addObserver:(NSObject *)observer 
//keyPath:被觀察的屬性
forKeyPath:(NSString *)keyPath 
//options:監聽選項,這個值可以是NSKeyValueObservingOptions選項的組合
//也就是可以這么寫(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
options:(NSKeyValueObservingOptions)options 
//context:同上面的方法
context:(nullable void *)context;

關于NSKeyValueObservingOptions,里面一共有四個值:

//設置后會在observeValueForKeyPath方法的change字典里存入更新后的值。
NSKeyValueObservingOptionNew
//設置后會在observeValueForKeyPath方法的change字典里存入更新前的值,也就是原有的值。
NSKeyValueObservingOptionOld
//設置后會在添加觀察者的時候立即發送一次通知給觀察者,并且在注冊觀察者方法之前返回。
//也就是在addObserver方法執行之后就立即發送了一次通知。
NSKeyValueObservingOptionInitial
//會在值被改變之前發送一次通知,并且在change字典里多了一個叫notificationIsPrior的key,值是1。
//而且change字典不會包含new(NSKeyValueChangeNewKey)這個key。
//當然值改變后的那次通知也會發,也就是說會發送兩次通知。
NSKeyValueObservingOptionPrior

當觀察者不再需要監聽屬性變化時,需要使用- (void)removeObserver:forKeyPath: 或者- (void)removeObserver:forKeyPath:context:來移除觀察者,需要注意的是如果移除了一個沒有觀察過的屬性,程序會拋出異常。也就是說如果之前觀察的是"property1",而在移除的時候keyPath參數寫的是"property2",這是就會有異常被拋出。可以使用@try/@catch來防止崩潰。

@try {
    [object removeObserver:observer forKeyPath:@"keyPath")];
}
@catch (NSException * __unused exception) {}
手動通知

默認情況下通知會被自動發送,但有的時候我們希望可以手動的控制它。這時候需要在被觀察對象的類里面重寫+ (BOOL)automaticallyNotifiesObserversForKey:方法。例如被觀察對象有一個屬性叫"bankCodeEn",我們希望這個屬性被修改時的通知由我們手動控制,就需要在被觀察對象的類文件里面這樣寫:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
// 如果屬性為bankCodeEn則關閉自動發送通知
BOOL automatic = YES;
if ([key isEqualToString:@"bankCodeEn"]) {
automatic = NO;
} else {
// 對于對象中其它沒有處理的屬性,我們需要調用[super automaticallyNotifiesObserversForKey:key],以避免無意中修改了父類的屬性的處理方式
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}

然后再對"bankCodeEn"屬性的setter方法做如下處理:
- (void)setBankCodeEn:(NSString *)bankCodeEn{
//當兩次賦予的值完全相等時,沒有必要再發送通知。這個if的條件語句可以根據實際需要自行修改,或者干脆不寫。
if (_bankCodeEn != bankCodeEn) {
[self willChangeValueForKey:@"bankCodeEn"];
_bankCodeEn = bankCodeEn;
[self didChangeValueForKey:@"bankCodeEn"];
}
}
注意 willChangeValueForKey:和didChangeValueForKey:方法在默認自動發送通知的情況下是由系統自動調用的,在手動通知時需要我們自己來調用,并且不應該重寫這兩個方法。

注冊依賴建

有時一個屬性的改變需要依賴其他的屬性,比如一個叫"fullName"的屬性,這個屬性依賴于"firstName"和"lastName"。

//fullName的getter方法
- (NSString *)fullName{
  return [NSString stringWithFormat:@"%@  %@", _firstName, _lastName];
}

這種情況下如果firstName發生了變化,fullName的值自然也會改變,但是由于沒有直接使用setter方法設置fullName,所以如果不做特殊設置的話KVO是不會發送通知的。

這種情況就需要使用注冊依賴建來解決。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"firstName", @"lastName"]];
}
return keyPaths;
}

這樣不論firstName,lastName,fullName中的哪個值放生了變化,監聽fullName的KVO都會被觸發。還可以使用這個方法來達到同樣的目的。
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}
這個方法的使用規則是+ (NSSet *)keyPathsForValuesAffecting + 屬性名(注意屬性名首字母大寫)。

屬性類型為集合的監聽

對于集合的KVO,我們需要了解的一點是,KVO旨在觀察關系(relationship)而不是集合。對于不可變集合屬性,我們更多的是把它當成一個整體來監聽,而無法去監聽集合中的某個元素的變化;對于可變集合屬性,實際上也是當成一個整體,去監聽它整體的變化,如添加、刪除和替換元素。

例如一個叫arr的NSArray類型屬性,我們可以使用集合代理對象(collection proxy object)來處理集合相關的操作。有下面的幾個代理方法需要實現

-countOf<Key>

// 以下兩者二選一
-objectIn<Key>AtIndex:
-<key>AtIndexes:

// 可選(增強性能)
-get<Key>:range:

具體實現如下
- (NSUInteger)countOfArr{
return [_arr count];
}
- (id)objectInArrAtIndex:(NSUInteger)index {
return [_arr objectAtIndex:index];
}

當我們使用對象的arr屬性時,通過[object valueForKey:@"arr"]來獲取該屬性,這個方法返回的代理數組對象支持所有正常的NSArray調用。換句話說,調用者并不知道返回的是一個真正的NSArray,還是一個代理的數組。

對于可變數組的操作

對于可變數組的代理對象,我們需要實現以下幾個方法:

// 至少實現一個插入方法和一個刪除方法
-insertObject:in<Key>AtIndex:
-removeObjectFrom<Key>AtIndex:
-insert<Key>:atIndexes:
-remove<Key>AtIndexes:

// 可選(增強性能)以下方法二選一
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>:

實現如下

- (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];
}

方法實現后,需要使用[object mutableArrayValueForKey:@"arr"]來訪問arr屬性才能或取到代理數組。在使用時訪問真正數組對象和集合代理對象差別還是很大的。

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是被觀察對象,PersonObject是觀察者對象。BankObject類里面有一個叫departments的可變數組屬性。

這段代碼只會觸發一次KVO,也就是只有在給departments賦予一個初始化數組的時候KVO被觸發,在給數組添加內容的時候并沒有觸發。

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];

使用集合代理對象的方式會觸發兩次KVO,在給數組插入(刪除,替換)數據的時候KVO也會被觸發。

監聽信息

對于被觀察的對象,可以使用observationInfo屬性獲取都有哪些觀察者觀察了哪些屬性。
id info = bankInstance.observationInfo;
NSLog(@"%@", [info description]);
如果像這樣獲取了一個被觀察對象的info然后打印出來,會看到這樣的結果。

<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指針實際上是指向一個NSKeyValueObservationInfo對象,它包含了指定對象上的所有的監聽信息。而每條監聽信息而是封裝在一個NSKeyValueObservance對象中,從上面可以看到,這個對象中包含消息的觀察者、被監聽的屬性、添加觀察者時所設置的一些選項、上下文信息等。

其他的一些小tips

1、如果重復添加注冊觀察者的方法(addObserver),比如像這樣完全一樣的兩句代碼重復兩次,那么通知也就會發送兩次,系統不會檢查也不會替換覆蓋。

[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、因為keyPath是字符串類型,這就導致寫錯的情況很容易發生,keyPath寫錯嚴重的話就會導致程序崩潰。所以為了避免這種情況,可以將@"property"替換成NSStringFromSelector(@selector(property)),這樣寫首先在敲屬性名的時候會有提示,而且在你把屬性名敲錯的時候由于xcode沒有在對應的類里面找到那個被你寫錯的屬性,就會報出警告。像這樣:

[bank addObserver:per1 forKeyPath:NSStringFromSelector(@selector(departments)) options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

3、關于context可以這樣設置,一個靜態變量存著它自己的指針。這意味著它自己什么也沒有。

static void * XXContext = &XXContext;
關于KVO的實現機制

KVO使用了OC的runtime來實現,在第一次觀察一個對象時,runtime會創建一個繼承自被觀察對象的類的子類,這個子類重寫了被觀察屬性的setter方法,然后將這個對象的is a指針指向了這個新建的類。也就是說其實這個被觀察的對象在程序運行時所屬的類已經不是之前我們自己寫的那個類了,而是系統創建的子類。

參考的文章:
Foundation: NSKeyValueObserving(KVO)
iOS KVO(鍵值觀察) 總覽
Key-Value Observing

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本文結構如下: Why? (為什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等開會閱讀 1,666評論 1 21
  • 本文由我們團隊的 糾結倫 童鞋撰寫。 文章結構如下: Why? (為什么要用KVO) What? (KVO是什么...
    知識小集閱讀 7,429評論 7 105
  • 或者KVO,是一個非正式協議,它定義了對象之間觀察和通知狀態改變的通用機制。 基本使用 使用KVO必須要滿足的條件...
    一枚iOS程序猿閱讀 758評論 0 0
  • 在編程中,最常見的就是程序的流程取決于你所使用的各種變量和屬性的值,根據變量和屬性的值確定后面運行的代碼,有時會檢...
    pro648閱讀 1,667評論 2 27
  • 前一段時間,對畫畫入了迷,晚上每有時間就拉著同事,向他學習素描。作為一名小白,對未知的領域充滿了虔誠之心,老師的句...
    MarcoH閱讀 421評論 1 0