回顧
在iOS的面試中除了KVC是經(jīng)常被問到的,還有KVO也是常問的,那么本篇博客就對KVO進行探索和分析下。

1. 什么是KVO
KVO 是 Objective-C對觀察者設計模式的一種實現(xiàn)。KVO提供一種機制,指定一個被觀察對象(例如A類),當對象某個屬性(例如A中的字符串name)發(fā)生更改時,對象會獲得通知,并作出相應處理;【且不需要給被觀察的對象添加任何額外代碼,就能使用KVO機制】。
一般繼承自NSObject的對象都默認支持KVO。KVO是響應式編程的代表。
2. KVO的使用
2.1 基本使用
- 注冊監(jiān)聽
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
observer:添加的監(jiān)聽者的對象,當監(jiān)聽的屬性發(fā)生改變時會通知這個對象。
keyPath:監(jiān)聽的屬性,不能傳nil。
options:指明通知發(fā)出的時機以及change中的鍵值。
context:是一個可選的參數(shù),可以傳任何數(shù)據(jù)。
- options
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,//更改前的值
NSKeyValueObservingOptionOld = 0x02,//更改后的值
NSKeyValueObservingOptionInitial = 0x04,//觀察最初的值(在注冊觀察服務時會調(diào)用一次觸發(fā)方法)
NSKeyValueObservingOptionPrior = 0x08 //分別在值修改前后觸發(fā)方法(即一次修改有兩次觸發(fā))
};
- 接收監(jiān)聽的屬性發(fā)生改變的通知
observeValueForKeyPath
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- 移除監(jiān)聽removeObserver
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
2.2 舉例
下面就簡單的舉個??,監(jiān)聽下
student屬性name的變化。
從控制臺的打印可以看出,在
KVO的監(jiān)聽回調(diào)observeValueForKeyPath方法里面,監(jiān)聽到了name屬性的變化,并打印出來變化信息。
- NSKeyValueChangeKey
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey;
NSKeyValueChangeKey指明了變更的類型,一般情況下返回的都1。集合中的元素被插入,刪除,替換時返回2、3、4
- NSKeyValueChange
定義如下:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//普通類型設置
NSKeyValueChangeInsertion = 2,//集合元素插入
NSKeyValueChangeRemoval = 3,//集合元素移除
NSKeyValueChangeReplacement = 4,//集合元素替換
};
-
NSKeyValueObservingOptionNew:指明change字典中應該包含改變后的新值。 -
NSKeyValueObservingOptionOld:指明change字典中應該包含改變前的舊值。 -
NSKeyValueObservingOptionInitial:注冊后立馬調(diào)用一次,這種通知只會發(fā)送一次??梢宰鲆恍┮淮涡缘墓ぷ鳌.斖瑫r指定new/old/initial的情況時,initial通知只包含new值。(實際上還是old值,因為是注冊后立馬調(diào)用,所以實際上對它來說是新值。任何情況下initial都不會包含old) -
NSKeyValueObservingOptionPrior:修改前后觸發(fā),會調(diào)用兩次。修改前觸發(fā)會包含notificationIsPrior字段。當同時指定new/old時,修改前會包含old,修改后會包含new和old。(一般的通知發(fā)出時機都是在屬性改變后,雖然change字典中包含了old和new,但是通知還是在屬性改變后才發(fā)出)。 -
0:直接傳遞0,在每次調(diào)用的時候都返回包含kind的change??梢岳斫鉃槟J實現(xiàn)。
- context
其他的見名知意,這個context 上下文,平時開發(fā)的時候都是直接寫個NULL,那么Ta有什么用呢?我們?nèi)ヌO果的KVO官方文檔看看。
從官方文檔的解釋來看就是:
使用
Context上下文,是一種更安全、更可擴展的方法來確保收到的通知是發(fā)送給我們的觀察者而不是superclass。
-
context會被傳遞到監(jiān)聽者的響應方法中,可以用來區(qū)分不同通知,也可以用來傳值。 - 對于多個
keyPath的觀察,需要在observeValueForKeyPath同時判斷object與keyPath,可以聲明一個靜態(tài)變量傳遞給context用來區(qū)分不同的通知提高代碼的可讀性。 - 如果子類和父類都實現(xiàn)了對同一對象的同一屬性的觀察,并且父類和子類都可能對其進行設值,那么這個時候就可以利用
context來進行區(qū)分了。
- 移除觀察者
我們平時使用KVO的時候,都會在頁面銷毀的時候移除觀察者,那么看看官方是如何解釋的。
- 當
deallocated時,觀察者不會自動刪除自己。 被觀察的對象會繼續(xù)發(fā)送通知,而忽略了觀察者的狀態(tài)。 - 然而,給一個已釋放de 對象發(fā)送任何其他的通知消息,會觸發(fā)
內(nèi)存訪問異常。 - 因此,要確保觀察者在從內(nèi)存中消失之前將它
移除。
例如: 當?shù)谝淮芜M入一個頁面的時候,我們注冊了觀察,然后通過某個觸摸事件觸發(fā)了回調(diào),接著我們退出了頁面。
當我們第二次進入這個頁面的時候,第一次注冊的觀察者已經(jīng)被銷毀,但是由于這個被觀察的對象是個單例,所以依舊會向其觀察的對象發(fā)送消息,最終導致內(nèi)存訪問異常,應用崩潰。
結(jié)論:當我們的頁面dealloc時,觀察者一定要移除,以防止內(nèi)存泄漏,出現(xiàn)指針。
2.3 自動/手動開啟KVO
- 自動開啟KVO
使用KVO時,默認情況下都是自動監(jiān)聽模式,而當我們想改變成手動監(jiān)聽模式的時候,我們需要在被監(jiān)聽的對象中實現(xiàn)automaticallyNotifiesObserversForKey方法
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
//可以根據(jù)不同的key值,來區(qū)分使用自動還是手動監(jiān)聽
if ([key isEqualToString:@"name"]) {
return YES;
}
return NO;
}
如果直接return NO則表示全部使用手動監(jiān)聽,這時候觸摸屏幕的事件就沒有任何響應了,如果想要響應則需要實現(xiàn)下面的方法。
- 手動開啟KVO
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
在willChangeValueForKey和didChangeValueForKey中間進行賦值,則會開啟手動監(jiān)聽模式。
2.4 觀察多個因素影響的屬性
有時候需要觀察的屬性,是由多個其他的因素共同影響而變化的。
例如在下載文件的過程,下載進度 = 已下載 / 總數(shù)。如果已下載和總數(shù)都是在不斷變化的,那么我們該怎么做才能對下載進度進行觀察呢?舉個??例子
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [JPPerson new];
[self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.writtenData += 10;
self.person.totalData += 20;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"ViewController :%@",change);
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"downloadProgress"];
}
-
點擊屏幕三次,分別打印結(jié)果如下
打印結(jié)果 - 除了第一次打印了三條數(shù)據(jù),其他都是兩次,為什么呢?那么去看看方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (NSString *)downloadProgress {
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
- 在
keyPathsForValuesAffectingValueForKey方法中,將和downloadProgress相關(guān)的兩個因素totalData和writtenData通過setByAddingObjectsFromArray關(guān)聯(lián)起來, - 那么每次
totalData或者writtenData改變時,系統(tǒng)會自動通知我們downloadProgress改變了。 - 而第一次的打印多打印了一次的原因是,當代碼執(zhí)行到
self.person.writtenData += 10賦值時,會走- (NSString *)downloadProgress方法,而此次totalData為0時,設置為100,當代碼執(zhí)行到self.person.totalData += 20;時,totalData就改變了兩次,就會走兩次監(jiān)聽方法,加上self.person.writtenData += 10賦值時writtenData的改變,一共就是三次了。
2.5 KVO對可變數(shù)組的觀察
例如對一個對象里面的數(shù)組進行監(jiān)聽:
self.person.dateArray = [NSMutableArray array];
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
[self.person.dateArray addObject:@"jay"];
}
在viewDidLoad中實現(xiàn)這些代碼,理論上進入頁面后就會觀察到并回調(diào),而實際上并沒有。于是去看蘋果的文檔,有了重大發(fā)現(xiàn),如下
In order to understand key-value observing, you must first understand key-value coding
這句話的意思,要想理解KVO就先要理解KVC,也就是說KVO是建立在KVC上的。
使用
KVO去觀察集合類型的數(shù)據(jù)變化,那么就需要使用對應的api來獲取這個集合,這樣在你進行設置值的時候,系統(tǒng)就能夠通知到你。
- 代碼修改之后
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [JPPerson new];
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
self.person.dateArray = [NSMutableArray array];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"jay"];
}
- 結(jié)果打印出來了,第一次是
dateArray初始化的打印,第二次是addObjcet添加數(shù)據(jù)的打印
數(shù)組KVO觀察
兩次打印的
kind值并不一樣,那么他們代表什么呢? 其實前面已經(jīng)說明過了,就是NSKeyValueChangeKey指明了變更的類型一般情況下返回的都
1。集合中的元素被插入,刪除,替換時返回2、3、4那么我們現(xiàn)在就驗證一下
從代碼驗證打印的結(jié)果可以看出,集合中的元素被插入,刪除,替換時返回
2、3、4。
3.總結(jié)
- 使用
KVO必須注冊觀察者。 - 使用
KVO在dealloc移除觀察者。 -
KVO自動還是手動開啟只要實現(xiàn)+ (BOOL)automaticallyNotifiesObserversForKey:方法,return NO表示手動,return YES表示自動。 - 要想理解KVO就先要理解KVC,也就是說KVO是建立在KVC上的。
更多內(nèi)容持續(xù)更新
?? 喜歡就點個贊吧????
?? 覺得有收獲的,可以來一波,收藏+關(guān)注,評論 + 轉(zhuǎn)發(fā),以免你下次找不到我????
??歡迎大家留言交流,批評指正,互相學習??,提升自我??