iOS底層探索之KVO(一)—KVO簡介

回顧

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

KVO

1. 什么是KVO

KVOObjective-C對觀察者設計模式的一種實現(xiàn)。KVO提供一種機制,指定一個被觀察對象(例如A類),當對象某個屬性(例如A中的字符串name)發(fā)生更改時,對象會獲得通知,并作出相應處理;【且不需要給被觀察的對象添加任何額外代碼,就能使用KVO機制】。

一般繼承自NSObject的對象都默認支持KVO。KVO是響應式編程的代表。

Key-Value Observing Programming Guide

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基本使用

從控制臺的打印可以看出,在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,修改后會包含newold。(一般的通知發(fā)出時機都是在屬性改變后,雖然change字典中包含了oldnew,但是通知還是在屬性改變后才發(fā)出)。
  • 0:直接傳遞0,在每次調(diào)用的時候都返回包含kindchange??梢岳斫鉃槟J實現(xiàn)。

- context

其他的見名知意,這個context 上下文,平時開發(fā)的時候都是直接寫個NULL,那么Ta有什么用呢?我們?nèi)ヌO果的KVO官方文檔看看。

Context官方文檔解釋

從官方文檔的解釋來看就是:

使用Context上下文,是一種更安全、更可擴展的方法來確保收到的通知是發(fā)送給我們的觀察者而不是superclass。

  • context會被傳遞到監(jiān)聽者的響應方法中,可以用來區(qū)分不同通知,也可以用來傳值。
  • 對于多個keyPath的觀察,需要在observeValueForKeyPath同時判斷objectkeyPath,可以聲明一個靜態(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"];

willChangeValueForKeydidChangeValueForKey中間進行賦值,則會開啟手動監(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)的兩個因素totalDatawrittenData通過setByAddingObjectsFromArray關(guān)聯(lián)起來,
  • 那么每次totalData或者writtenData改變時,系統(tǒng)會自動通知我們downloadProgress改變了。
  • 而第一次的打印多打印了一次的原因是,當代碼執(zhí)行到self.person.writtenData += 10賦值時,會走- (NSString *)downloadProgress方法,而此次totalData0時,設置為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)在就驗證一下

NSKeyValueChangeKey驗證

從代碼驗證打印的結(jié)果可以看出,集合中的元素被插入,刪除,替換時返回23、4。

3.總結(jié)

  • 使用KVO必須注冊觀察者。
  • 使用KVOdealloc移除觀察者。
  • KVO自動還是手動開啟只要實現(xiàn)+ (BOOL)automaticallyNotifiesObserversForKey:方法,return NO表示手動,return YES表示自動。
  • 要想理解KVO就先要理解KVC,也就是說KVO是建立在KVC上的。

更多內(nèi)容持續(xù)更新

?? 喜歡就點個贊吧????

?? 覺得有收獲的,可以來一波,收藏+關(guān)注,評論 + 轉(zhuǎn)發(fā),以免你下次找不到我????

??歡迎大家留言交流,批評指正,互相學習??,提升自我??

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容