KVO深入解析.md

iOS 中監(jiān)聽某個值的改變有哪些方法店溢?

在一個復(fù)雜的叁熔,有狀態(tài)的系統(tǒng)中,當(dāng)一個對象的狀態(tài)發(fā)生改變床牧,如何通知系統(tǒng)荣回,并對狀態(tài)改變做出相應(yīng)的行為是必需考慮的一個問題,在iOS中為這類問題提供了4種解決方法:

  1. NSNotifiactaion 和 NSNotificationCenter:通知中心

  2. Delegates:代理戈咳,

  3. Callback:回調(diào)心软,

  4. KVO(Key-Value Observing):鍵值觀察

Key-Value Observing (簡寫為KVO):當(dāng)指定的對象的屬性被修改了,允許對象接受到通知的機(jī)制著蛙。每次指定的被觀察對象的屬性被修改的時候删铃,KVO都會自動的去通知相應(yīng)的觀察者。

KVO 是什么踏堡?

Objective-C 中的鍵(key)-值(value)觀察(KVO)并不是什么新鮮事物猎唁,它來源于設(shè)計模式中的觀察者模式,其基本思想就是:
一個目標(biāo)對象管理所有依賴于它的觀察者對象顷蟆,并在它自身的狀態(tài)改變時主動通知觀察者對象诫隅。這個主動通知通常是通過調(diào)用各觀察者對象所提供的接口方法來實現(xiàn)的。觀察者模式較完美地將目標(biāo)對象與觀察者對象解耦帐偎。

KVO 的用法

  1. 如果要監(jiān)聽“對象A”屬性值的改變逐纬,先要為"對象A"的屬性注冊觀察者(假設(shè)觀察者為“對象B”)。

- (void)addObserver:(NSObject * _Nonnull)anObserver
forKeyPath:(NSString * _Nonnull)keyPath
options:(NSKeyValueObservingOptions)options
context:(void * _Nullable)context

anObserver : 觀察者削樊,注冊 KVO 通知的對象. 其必須實現(xiàn)方法 observeValueForKeyPath:ofObject:change:context:.
keyPath : 被觀察的屬性豁生,其不能為nil.
options : 表示要監(jiān)聽那些通知,一般為寫 0
context : 一些其他的需要傳遞給觀察者的上下文信息,通常設(shè)置為 nil

options 解釋如下:
NSKeyValueObservingOptionNew : change 字典中包含 key 改變后的新值
NSKeyValueObservingOptionOld : change 字典中包含 key 改變前的舊值
NSKeyValueObservingOptionInitial : 在添加觀察者的時候立即發(fā)送一個通知給觀察者,并且是在注冊觀察者方法返回之前
NSKeyValueObservingOptionPrior : 如果指定甸箱,則在每次修改屬性時眼刃,會在修改通知被發(fā)送之前預(yù)先發(fā)送一條通知給觀察者,這與-willChangeValueForKey:被觸發(fā)的時間是相對應(yīng)的摇肌。這樣擂红,在每次修改屬性時,實際上是會發(fā)送兩條通知围小。
2. 觀察者“對象B”實現(xiàn) observeValueForKeyPath:ofObject:change:context:. 方法
3. “對象A”移除監(jiān)聽者 - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
另外注意:keyPath 即被觀察的屬性只能是 NSString 類型

  • 注意點:在哪個線程觸發(fā)監(jiān)聽(修改了值)昵骤,監(jiān)聽方法就會在哪個線程中執(zhí)行

KVO 的實現(xiàn)原理

鍵值編碼和鍵值觀察是根據(jù) isa-swizzling 技術(shù)來實現(xiàn)的,主要依據(jù)runtime的強(qiáng)大動態(tài)能力肯适。
當(dāng)某個類的對象第一次被觀察時变秦,系統(tǒng)就會在運行期動態(tài)地創(chuàng)建該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法框舔。派生類在被重寫的 setter 方法實現(xiàn)真正的通知機(jī)制蹦玫。
同時派生類還重寫了 class 方法以“欺騙”外部調(diào)用者它就是起初的那個類。然后系統(tǒng)將這個對象的 isa 指針指向這個新誕生的派生類刘绣,因此這個對象就成為該派生類的對象了樱溉,因而在該對象上對 setter 的調(diào)用就會調(diào)用重寫的 setter,從而激活鍵值通知機(jī)制纬凤。此外福贞,派生類還重寫了 dealloc 方法來釋放資源。

  1. 重寫新類的 class 方法
    重寫class方法是為了我們調(diào)用它的時候返回跟重寫繼承類之前同樣的內(nèi)容停士。
//打印如下內(nèi)容:
NSLog(@"self->isa:%@",self->isa);
NSLog(@"self class:%@",[self class]);

//在建立KVO監(jiān)聽前挖帘,打印結(jié)果為:
self->isa:Person
self class:Person

//在建立KVO監(jiān)聽之后,打印結(jié)果為:
self->isa:NSKVONotifying_Person

self class:Person
  1. 重寫新類的 set 方法
    新類會重寫對應(yīng)的set方法恋技,是為了在set方法中增加另外兩個方法的調(diào)用:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

其中拇舀,didChangeValueForKey:方法負(fù)責(zé)調(diào)用:

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context

如果沒有 setter 方法,那么 -setValue:forKey 方法會直接調(diào)用:

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

如果在沒有使用鍵值編碼且沒有使用適當(dāng)命名的訪問起方法的時候蜻底,我們只需要顯示調(diào)用上述兩個方法骄崩,同樣可以使用KVO!

例子

//
//  ViewController.m
//  KVO test
//
//  Created by KeSen on 15/9/1.
//  Copyright (c) 2015年 KeSen. All rights reserved.
//

#import "ViewController.h"
#import "KSBaby.h"

@interface ViewController ()
{
KSBaby *_baby;
}
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

KSBaby *baby = [[KSBaby alloc] initWithHungry:@"cryed" thirst:NO];
_baby = baby;

// 一般如下使用
// [_baby addObserver:self forKeyPath:@"cry" options: 0 context: nil];
// 在 self 中監(jiān)聽 _baby 的 cry 屬性變化
[_baby addObserver:self forKeyPath:@"cry" options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld| NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior context:@"ssss"];
}

- (void)dealloc {
[_baby removeObserver:self forKeyPath:@"cry"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"keyPath: %@\n change: %@\n context: %@\n", keyPath, change, context);
}

- (IBAction)click:(UIButton *)sender {
_baby.cry = @"crying";
}
@end
//
//  ViewController.m
//  KVO test
//
//  Created by KeSen on 15/9/1.
//  Copyright (c) 2015年 KeSen. All rights reserved.
//

#import "ViewController.h"
#import "KSBaby.h"

@interface ViewController ()
{
KSBaby *_baby;
}
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

KSBaby *baby = [[KSBaby alloc] initWithHungry:@"cryed" thirst:NO];
_baby = baby;

// 一般如下使用
// [_baby addObserver:self forKeyPath:@"cry" options: 0 context: nil];
// 在 self 中監(jiān)聽 _baby 的 cry 屬性變化
[_baby addObserver:self forKeyPath:@"cry" options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld| NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior context:@"ssss"];
}

- (void)dealloc {
[_baby removeObserver:self forKeyPath:@"cry"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"keyPath: %@\n change: %@\n context: %@\n", keyPath, change, context);
}

- (IBAction)click:(UIButton *)sender {
_baby.cry = @"crying";
}
@end

那么:程序啟動時輸出:

image.png


點擊按鈕后輸出:


6aeaf4e75e531bd372f4a322860cbf07.png

KVO與多線程

注意點:在哪個線程觸發(fā)監(jiān)聽(修改了值)朱躺,監(jiān)聽方法就會在哪個線程中執(zhí)行

//
//  ViewController.m
//  KVO test
//
//  Created by KeSen on 15/9/1.
//  Copyright (c) 2015年 KeSen. All rights reserved.
//

#import "ViewController.h"
#import "KSBaby.h"
#import "FBKVOController.h"

@interface ViewController ()
{
KSBaby *_baby;
FBKVOController *_observer;
}
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

KSBaby *baby = [[KSBaby alloc] initWithHungry:@"cryed" thirst:NO];
_baby = baby;

// NSKeyValueObservingOptionNew : change 字典中包含 key 改變后的新值
// NSKeyValueObservingOptionOld : change 字典中包含 key 改變前的舊值
// NSKeyValueObservingOptionInitial : 在添加觀察者的時候立即發(fā)送一個通知給觀察者,并且是在注冊觀察者方法返回之前
// NSKeyValueObservingOptionPrior : 如果指定刁赖,則在每次修改屬性時,會在修改通知被發(fā)送之前預(yù)先發(fā)送一條通知給觀察者长搀,這與-willChangeValueForKey:被觸發(fā)的時間是相對應(yīng)的宇弛。這樣,在每次修改屬性時源请,實際上是會發(fā)送兩條通知枪芒。

// 1. 一般用法如下
[_baby addObserver:self forKeyPath:@"cry" options: 0 context: nil];

//    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//        [_baby addObserver:self forKeyPath:@"cry" options: NSKeyValueObservingOptionNew context:@"ssss"];
//
//    });

// 2. 第三方庫 FBKVOController 的用法
_observer = [[FBKVOController alloc] initWithObserver:self];
[_observer observe:_baby keyPath:@"cry" options:NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {

NSLog(@"%@, %@", @"FBKVOController", change[NSKeyValueChangeNewKey]);
}];
}

- (void)dealloc {
[_baby removeObserver:self forKeyPath:@"cry"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"keyPath: %@\n change: %@\n context: %@\n", keyPath, change, context);
}

- (IBAction)click:(UIButton *)sender {
//    _baby.cry = @"crying";

// 1. 注意點:在哪個線程觸發(fā)監(jiān)聽(修改了值)彻况,監(jiān)聽方法就會在哪個線程中執(zhí)行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
_baby.cry = @"cry again";
});
}

@end

KVO 監(jiān)聽 NSMutableArray 的內(nèi)容變化

  1. 將數(shù)組封裝到一個對象中
  2. 給這個對象添加 KVO 監(jiān)聽
  3. 使用 [self.arrayObject mutableArrayValueForKey:@"array”] 獲取數(shù)組對象,對數(shù)組對象今天添加刪除操作舅踪,只有這樣觸發(fā)的數(shù)組才會觸發(fā) kvo


    06f37246739a3b062139a12c5070e044.png

參考:http://www.cppblog.com/kesalin/archive/2012/11/17/kvo.html
http://ningandjiao.iteye.com/blog/2009729
http://www.bkjia.com/IOSjc/993206.html
http://blog.csdn.net/wzzvictory/article/details/9674431

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纽甘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子抽碌,更是在濱河造成了極大的恐慌悍赢,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件货徙,死亡現(xiàn)場離奇詭異左权,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)痴颊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門赏迟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蠢棱,你說我怎么就攤上這事锌杀。” “怎么了泻仙?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵糕再,是天一觀的道長。 經(jīng)常有香客問我饰豺,道長亿鲜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任冤吨,我火速辦了婚禮,結(jié)果婚禮上饶套,老公的妹妹穿的比我還像新娘漩蟆。我一直安慰自己,他們只是感情好妓蛮,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布怠李。 她就那樣靜靜地躺著,像睡著了一般蛤克。 火紅的嫁衣襯著肌膚如雪捺癞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天构挤,我揣著相機(jī)與錄音髓介,去河邊找鬼。 笑死筋现,一個胖子當(dāng)著我的面吹牛唐础,可吹牛的內(nèi)容都是我干的箱歧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼一膨,長吁一口氣:“原來是場噩夢啊……” “哼呀邢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起豹绪,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤价淌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后瞒津,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體输钩,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年仲智,在試婚紗的時候發(fā)現(xiàn)自己被綠了买乃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡钓辆,死狀恐怖剪验,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情前联,我是刑警寧澤功戚,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站似嗤,受9級特大地震影響啸臀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烁落,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一乘粒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧伤塌,春花似錦灯萍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至药薯,卻和暖如春绑洛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背童本。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工真屯, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人巾陕。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓讨跟,卻偏偏與公主長得像纪他,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子晾匠,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容