KVO 正確使用姿勢(shì)進(jìn)階及底層實(shí)現(xiàn)

你要知道的KVC楞卡、KVO崭别、Delegate树酪、Notification都在這里

轉(zhuǎn)載請(qǐng)注明出處 http://www.reibang.com/p/d3bfa1e9fa0a

本系列文章主要通過(guò)講解KVC择镇、KVO、Delegate影暴、Notification的使用方法错邦,來(lái)探討KVO、Delegate型宙、Notification的區(qū)別以及相關(guān)使用場(chǎng)景撬呢,本系列文章將分一下幾篇文章進(jìn)行講解,讀者可按需查閱妆兑。

KVO 正確使用姿勢(shì)進(jìn)階及底層實(shí)現(xiàn)

KVO(key value observing)鍵值監(jiān)聽(tīng)是我們?cè)陂_(kāi)發(fā)中常使用的用于監(jiān)聽(tīng)特定對(duì)象屬性值變化的方法搁嗓,常用于監(jiān)聽(tīng)數(shù)據(jù)模型的變化從而可以動(dòng)態(tài)的修改對(duì)應(yīng)視圖芯勘。能夠上述需求的方法有很多,后面要講的DelegateNotification都可以實(shí)現(xiàn)谱姓,但都有各自的優(yōu)缺點(diǎn)和適用場(chǎng)景借尿,需要根據(jù)實(shí)際情況按需選擇刨晴,但三者都很重要屉来,在開(kāi)發(fā)中都會(huì)使用。

KVC相同狈癞,OC在實(shí)現(xiàn)KVO時(shí)沒(méi)有采用實(shí)現(xiàn)接口的方式茄靠,而是針對(duì)NSObject創(chuàng)建了一個(gè)類(lèi)別,通過(guò)這樣的方式使得NSObject的子類(lèi)可以自行實(shí)現(xiàn)NSKeyValueObserving類(lèi)別定義的相關(guān)方法蝶桶,其他的如NSArray慨绳、NSSet這樣的集合類(lèi)也都定義了相關(guān)的類(lèi)別,因此也可以對(duì)集合類(lèi)型進(jìn)行KVO的監(jiān)聽(tīng)真竖。本文主要進(jìn)行KVO進(jìn)階講解脐雪,基礎(chǔ)知識(shí)還需讀者自行查閱。

學(xué)習(xí)KVO最好的方法就是閱讀官方文檔:Key-Value Observing Programming Guide

KVO基礎(chǔ)方法詳解進(jìn)階

KVO常用的方法有如下幾個(gè):

/*
注冊(cè)監(jiān)聽(tīng)器
監(jiān)聽(tīng)器對(duì)象為observer恢共,被監(jiān)聽(tīng)對(duì)象為消息的發(fā)送者即方法的調(diào)用者在回調(diào)函數(shù)中會(huì)被回傳
監(jiān)聽(tīng)的屬性路徑為keyPath支持點(diǎn)語(yǔ)法的嵌套
監(jiān)聽(tīng)類(lèi)型為options支持按位或來(lái)監(jiān)聽(tīng)多個(gè)事件類(lèi)型
監(jiān)聽(tīng)上下文context主要用于在多個(gè)監(jiān)聽(tīng)器對(duì)象監(jiān)聽(tīng)相同keyPath時(shí)進(jìn)行區(qū)分
添加監(jiān)聽(tīng)器只會(huì)保留監(jiān)聽(tīng)器對(duì)象的地址战秋,不會(huì)增加引用,也不會(huì)在對(duì)象釋放后置空讨韭,因此需要自己持有監(jiān)聽(tīng)對(duì)象的強(qiáng)引用脂信,該參數(shù)也會(huì)在回調(diào)函數(shù)中回傳
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

/*
刪除監(jiān)聽(tīng)器
監(jiān)聽(tīng)器對(duì)象為observer癣蟋,被監(jiān)聽(tīng)對(duì)象為消息的發(fā)送者即方法的調(diào)用者,應(yīng)與addObserver方法匹配
監(jiān)聽(tīng)的屬性路徑為keyPath狰闪,應(yīng)與addObserver方法的keyPath匹配
監(jiān)聽(tīng)上下文context疯搅,應(yīng)與addObserver方法的context匹配
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

/*
與上一個(gè)方法相同,只是少了context參數(shù)
推薦使用上一個(gè)方法埋泵,該方法由于沒(méi)有傳遞context可能會(huì)產(chǎn)生異常結(jié)果
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/*
監(jiān)聽(tīng)器對(duì)象的監(jiān)聽(tīng)回調(diào)方法
keyPath即為監(jiān)聽(tīng)的屬性路徑
object為被監(jiān)聽(tīng)的對(duì)象
change保存被監(jiān)聽(tīng)的值產(chǎn)生的變化
context為監(jiān)聽(tīng)上下文幔欧,由add方法回傳
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

舉一個(gè)簡(jiǎn)單的栗子:

#import <Foundation/Foundation.h>

@interface Account: NSObject

@property (nonatomic, copy) NSString *accountNumber;
@property (nonatomic, assign) double balance;

@end

@implementation Account

@synthesize accountNumber = _accountNumber;
@synthesize balance = _balance;

@end

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, strong) Account *account;

- (void)setObserver;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;

//添加監(jiān)聽(tīng)器
- (void)setObserver
{
    /*
    監(jiān)聽(tīng)器對(duì)象為Person類(lèi)的對(duì)象本身,被監(jiān)聽(tīng)的對(duì)象為Person類(lèi)對(duì)象持有的account
    監(jiān)聽(tīng)的屬性路徑為account的balance丽声,可以監(jiān)聽(tīng)嵌套的對(duì)象比如account有一個(gè)對(duì)象是bank可以監(jiān)聽(tīng)bank是否營(yíng)業(yè)琐馆,可以寫(xiě)"bank.isOpen"
    監(jiān)聽(tīng)上下文設(shè)置為nil,相信很多人在使用的時(shí)候都會(huì)這么寫(xiě)
    */
    [self.account addObserver:self forKeyPath:@"balance" options:NSKeyValueObservingOptionNew context:nil];
}

//監(jiān)聽(tīng)器回調(diào)方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    //判斷被監(jiān)聽(tīng)對(duì)象是否為account恒序,并且通過(guò)NSString來(lái)判斷監(jiān)聽(tīng)屬性路徑是否一致
    if (object == self.account && [keyPath isEqualToString:@"balance"])
    {
        NSLog(@"NewBalance: %lf", self.account.balance);
    }
}

//Person銷(xiāo)毀時(shí)調(diào)用的方法
- (void)dealloc
{
    /*
    切記瘦麸,當(dāng)我們添加監(jiān)聽(tīng)器時(shí)一定要在對(duì)象被銷(xiāo)毀前刪除該監(jiān)聽(tīng)器
    刪除監(jiān)聽(tīng)器傳遞的參數(shù)要與添加監(jiān)聽(tīng)器傳參一致
    監(jiān)聽(tīng)器也不可以重復(fù)刪除,如果沒(méi)有注冊(cè)監(jiān)聽(tīng)器而去執(zhí)行刪除操作也會(huì)拋出異常
    */
    [self.account removeObserver:self forKeyPath:@"balance" context:nil];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        
        p.account = [[Account alloc] init];
        p.account.balance = 100.0;
        //添加監(jiān)聽(tīng)器
        [p setObserver];
        //重新對(duì)account的balance賦值后會(huì)觸發(fā)回調(diào)函數(shù)
        //輸出: NewBalance: 200.0
        p.account.balance = 200.0;        
    }
    return 0;
}

上面的例子很簡(jiǎn)單歧胁,運(yùn)行結(jié)果也很正常滋饲,在Person類(lèi)對(duì)象被銷(xiāo)毀前也進(jìn)行了監(jiān)聽(tīng)器的刪除操作,并且運(yùn)行結(jié)果也很正常喊巍,相信很多人在實(shí)際的開(kāi)發(fā)過(guò)程中也都是按照這樣方式實(shí)現(xiàn)的KVO屠缭,不幸的是,上面的寫(xiě)法有很多缺陷崭参。

首先呵曹,講解一下為什么要在對(duì)象被銷(xiāo)毀前刪除監(jiān)聽(tīng)器,我們?cè)陂_(kāi)發(fā)中使用KVO時(shí)很可能會(huì)遇到因?yàn)闆](méi)有刪除監(jiān)聽(tīng)器而產(chǎn)生的野指針錯(cuò)誤何暮。

KVO在注冊(cè)監(jiān)聽(tīng)器的時(shí)候不會(huì)持有監(jiān)聽(tīng)器對(duì)象的引用奄喂,也不會(huì)像weak那樣在監(jiān)聽(tīng)器對(duì)象被銷(xiāo)毀時(shí)置nil,而是僅僅保留監(jiān)聽(tīng)器對(duì)象的地址海洼,類(lèi)似于copy修飾符跨新,當(dāng)監(jiān)聽(tīng)器對(duì)象被銷(xiāo)毀而又沒(méi)有刪除監(jiān)聽(tīng)器時(shí),如果這個(gè)時(shí)候被監(jiān)聽(tīng)對(duì)象的值發(fā)生變化系統(tǒng)會(huì)執(zhí)行監(jiān)聽(tīng)器的回調(diào)函數(shù)坏逢,這個(gè)時(shí)候監(jiān)聽(tīng)器對(duì)象已經(jīng)不存在了域帐,KVO保留的地址就是一個(gè)野指針,因此會(huì)產(chǎn)生野指針錯(cuò)誤是整。上面的栗子由于在對(duì)象被銷(xiāo)毀前沒(méi)有修改account.balance的值肖揣,因此哪怕不刪除監(jiān)聽(tīng)器也不會(huì)產(chǎn)生野指針異常,但我們需要注意的是浮入,要時(shí)刻保證addObserverremoveObserver成對(duì)出現(xiàn)龙优,避免野指針錯(cuò)誤的產(chǎn)生。

接下來(lái)舉一個(gè)會(huì)產(chǎn)生野指針異常的栗子:

/*
首先實(shí)現(xiàn)兩個(gè)UIViewController
以下代碼為ViewController代碼舵盈,在ViewController中添加兩個(gè)按鈕陋率,并分別添加兩個(gè)點(diǎn)擊事件球化。其他代碼不再展示,讀者可自行完善
*/

//第一個(gè)按鈕點(diǎn)擊處理器
- (void)buttonClicked
{
    /*
    另一個(gè)UIViewController為DisplayViewController
    在開(kāi)發(fā)中經(jīng)常會(huì)遇到這樣的情形瓦糟,需要?jiǎng)?chuàng)建一個(gè)VC來(lái)展示Model的數(shù)據(jù)
    以下兩行代碼就是用來(lái)創(chuàng)建并展示該VC
    */
    DisplayViewController *vc = [[DisplayViewController alloc] initWithModel:self.model];
    [self presentViewController:vc animated:YES completion:nil];
}

//第二個(gè)按鈕點(diǎn)擊處理器
- (void)button2Clicked
{
    //模擬模型數(shù)據(jù)發(fā)生變化
    self.model.balance = 8888;
}


/*
接下來(lái)實(shí)現(xiàn)DisplayViewController
假設(shè)DisplayViewController中需要對(duì)Model進(jìn)行進(jìn)一步處理筒愚,所以需要監(jiān)聽(tīng)Model的balance屬性,并在initWithModel:初始化方法中添加監(jiān)聽(tīng)器
*/
//初始化方法菩浙,添加一個(gè)退出按鈕巢掺,并添加model的balance屬性監(jiān)聽(tīng)器
- (instancetype)initWithModel:(Model*)model;
{
    if (self = [super init])
    {
        self.view.backgroundColor = [UIColor whiteColor];
        
        self.model = model;
        
        //創(chuàng)建監(jiān)聽(tīng)器
        [self.model addObserver:self forKeyPath:@"balance" options:NSKeyValueObservingOptionInitial context:nil];
        
        self.exitButton = [UIButton buttonWithType:UIButtonTypeCustom];
        self.exitButton.frame = CGRectMake(150, 200, 80, 80);
        self.exitButton.backgroundColor = [UIColor blackColor];
        [self.exitButton addTarget:self action:@selector(exitButtonClickedHandler) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:self.exitButton];
    }
    return self;
}
//退出按鈕處理器
- (void)exitButtonClickedHandler
{
    //直接退出當(dāng)前頁(yè)面
    [self dismissViewControllerAnimated:YES completion:nil];
}

//監(jiān)聽(tīng)model的balance屬性
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if (object == self.model && [keyPath isEqualToString:@"balance"])
    {
        NSLog(@"New Balance %lf", self.model.balance);
    }
}

//以下dealloc方法注釋?zhuān)虼水?dāng)DisplayViewController銷(xiāo)毀時(shí)不會(huì)刪除監(jiān)聽(tīng)器
//- (void)dealloc
//{
//    [self.model removeObserver:self forKeyPath:@"balance" context:nil];
//}

上述代碼完成后,運(yùn)行程序劲蜻,ViewController頁(yè)面如下:

ViewController頁(yè)面

該視圖只有兩個(gè)按鈕陆淀,Click Me為第一個(gè)按鈕,點(diǎn)擊后觸發(fā)buttonClicked方法先嬉,該方法創(chuàng)建DisplayViewController后直接展示出來(lái)轧苫,DisplayViewController頁(yè)面如下:

DisplayViewController頁(yè)面

該視圖只有一個(gè)按鈕,點(diǎn)擊黑色按鈕后退出頁(yè)面疫蔓,回到ViewController視圖中含懊,此時(shí)并沒(méi)有任何錯(cuò)誤產(chǎn)生,盡管我們?cè)?code>DisplayViewController銷(xiāo)毀后也沒(méi)有刪除其監(jiān)聽(tīng)器衅胀,這個(gè)邏輯在開(kāi)發(fā)中經(jīng)常遇到岔乔,在一個(gè)頁(yè)面獲取到數(shù)據(jù)后使用另一個(gè)頁(yè)面來(lái)展示相關(guān)數(shù)據(jù),另一個(gè)頁(yè)面很有可能會(huì)根據(jù)需求來(lái)監(jiān)聽(tīng)模型對(duì)象滚躯。此時(shí)如果點(diǎn)擊第二個(gè)按鈕BTN2不幸的事情就會(huì)產(chǎn)生雏门,在button2Clicked方法中會(huì)產(chǎn)生野指針錯(cuò)誤,因?yàn)樵谠摲椒ㄖ行薷牧?code>model.balance的值掸掏,由于前一個(gè)視圖中沒(méi)有刪除監(jiān)聽(tīng)器茁影,KVO中仍然有監(jiān)聽(tīng)器的存在,此時(shí)會(huì)觸發(fā)監(jiān)聽(tīng)器的回調(diào)方法阅束,但DisplayViewController早已銷(xiāo)毀呼胚,因此產(chǎn)生野指針錯(cuò)誤,當(dāng)我們把DisplayViewControllerdealloc方法去掉注釋后一切運(yùn)行正常息裸,因?yàn)樵?code>DisplayViewController銷(xiāo)毀時(shí)也刪除了監(jiān)聽(tīng)器。

上面這個(gè)栗子產(chǎn)生的野指針錯(cuò)誤正是因?yàn)?code>KVO使用不正確沪编,可能有些讀者沒(méi)有在監(jiān)聽(tīng)器銷(xiāo)毀前刪除監(jiān)聽(tīng)器也沒(méi)有發(fā)生過(guò)任何異常呼盆,因此不太注意,但KVO正確使用姿勢(shì)一定是在監(jiān)聽(tīng)器對(duì)象銷(xiāo)毀前刪除監(jiān)聽(tīng)器蚁廓。

上面的例子看似解決了一個(gè)問(wèn)題访圃,需要注意的是上面的栗子在創(chuàng)建監(jiān)聽(tīng)器時(shí)傳入的contextnil,可能很多初學(xué)者都會(huì)這么寫(xiě)相嵌,接下來(lái)繼續(xù)看一個(gè)栗子:

/*
本示例與上一個(gè)栗子相同腿时,只是在ViewController中注冊(cè)了model.balance的監(jiān)聽(tīng)器
*/
//ViewController.m
//在初始化時(shí)注冊(cè)model.balance監(jiān)聽(tīng)器

/*
DisplayViewController與上一個(gè)栗子一樣况脆,但多添加一個(gè)按鈕
*/
- (void)changeValueButtonClickedHandler
{
    self.model.balance = 8989;
}

上面這個(gè)栗子與前一個(gè)類(lèi)似,只不過(guò)在ViewController中同樣添加了對(duì)model.balance的監(jiān)聽(tīng)批糟,也就是說(shuō)兩個(gè)ViewControllerDisplayViewController都監(jiān)聽(tīng)了同一個(gè)對(duì)象的屬性值格了,這在開(kāi)發(fā)中也很常見(jiàn),在DisplayViewController中添加了一個(gè)按鈕用于模擬在DisplayViewController中修改model.balance值的操作徽鼎,現(xiàn)在兩個(gè)視圖都監(jiān)聽(tīng)了同一對(duì)象的屬性值盛末,那當(dāng)我們展示DisplayViewController后修改了model.balance的值,此時(shí)會(huì)觸發(fā)哪個(gè)視圖的回調(diào)函數(shù)呢否淤?實(shí)驗(yàn)一下就能發(fā)現(xiàn)兩個(gè)視圖的監(jiān)聽(tīng)器回調(diào)函數(shù)都觸發(fā)了悄但。

KVO還有一個(gè)可能會(huì)產(chǎn)生錯(cuò)誤的地方,在看下一個(gè)栗子之前有一點(diǎn)需要說(shuō)明石抡,有時(shí)候我們可能在一個(gè)視圖中監(jiān)聽(tīng)很多模型對(duì)象檐嚣,當(dāng)然了可以按照我們常用的通過(guò)keyPath字符串來(lái)判斷產(chǎn)生回調(diào)的具體是哪個(gè)屬性值,但如果監(jiān)聽(tīng)很多屬性值啰扛,這樣的方法似乎看起來(lái)很凌亂净嘀,而且逐一進(jìn)行字符串判斷感覺(jué)很浪費(fèi)資源,并且當(dāng)我們?cè)诤笃谛薷牧藢傩缘拿Q還不能忘記修改監(jiān)聽(tīng)器的keyPath判斷語(yǔ)句侠讯,那有什么辦法能夠取代keyPath嗎挖藏?答案是context,初學(xué)者經(jīng)常直接將context置為nil厢漩,但context才是KVO保證正確運(yùn)行的關(guān)鍵膜眠。

context是一個(gè)id類(lèi)型的參數(shù),在注冊(cè)監(jiān)聽(tīng)器時(shí)可以傳入該參數(shù)溜嗜,在回調(diào)函數(shù)中會(huì)回傳該參數(shù)宵膨,因此,該參數(shù)就能完美的解決上述兩個(gè)問(wèn)題炸宵。那context這個(gè)id類(lèi)型的參數(shù)設(shè)置為什么值比較合適呢辟躏?可能第一感覺(jué)還是設(shè)置為NSString類(lèi)型,但這樣仍然可能會(huì)產(chǎn)生沖突土全,蘋(píng)果推薦的做法是創(chuàng)建一個(gè)靜態(tài)變量然后使用該靜態(tài)變量的地址作為context捎琐,通過(guò)這樣的方法就能夠保證context的獨(dú)一無(wú)二。

接下來(lái)看下一個(gè)栗子:

/*
本栗子需要使用三個(gè)UIViewController
ViewController根視圖控制器

DisplayViewController 父視圖控制器
SubViewController 子視圖控制器

ViewController不監(jiān)聽(tīng)模型裹匙,包括一個(gè)按鈕用于創(chuàng)建SubViewController并展示

DisplayViewController還是之前栗子的

SubViewController繼承DisplayViewController并且也創(chuàng)建了監(jiān)聽(tīng)器來(lái)監(jiān)聽(tīng)model.balance屬性
*/

//ViewController部分代碼如下
//該控制器只有一個(gè)按鈕
- (void)buttonClicked
{
    SubViewController *vc = [[SubViewController alloc] initWithModel:self.model];
    [self presentViewController:vc animated:YES completion:nil];
}

//DisplayViewController的部分代碼如下
//為了便于輸出這里使用的是NSString類(lèi)型的context
static void * DisplayViewControllerBalanceObserverContext = @"DDDDDDDD";

//在初始化方法中輸入上面的變量作為context進(jìn)行監(jiān)聽(tīng)器的注冊(cè)
[self.model addObserver:self forKeyPath:@"balance" options:NSKeyValueObservingOptionInitial context:DisplayViewControllerBalanceObserverContext];

//退出按鈕方法
- (void)exitButtonClickedHandler
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

//模擬修改模型數(shù)據(jù)變化的按鈕
- (void)changeValueButtonClickedHandler
{
    self.model.balance = 8989;
}

//監(jiān)聽(tīng)器回調(diào)函數(shù)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    //將void *的context轉(zhuǎn)換為NSString類(lèi)型
    NSString *d = (__bridge NSString*)context;
    NSLog(@"DIS %@", d);
    
    if (context == DisplayViewControllerBalanceObserverContext)
    {
        NSLog(@"DDD New Balance %lf", self.model.balance);
    }
}

//刪除監(jiān)聽(tīng)器
- (void)dealloc
{
    [self.model removeObserver:self forKeyPath:@"balance" context:DisplayViewControllerBalanceObserverContext];
}

//SubViewController部分代碼如下
//為了便于輸出使用NSString類(lèi)型的context
static void * SubViewControllerBalanceObserverContext = @"CCCCCCCAAAA";

- (instancetype)initWithModel:(Model *)model;
{
    if (self = [super initWithModel:model])
    {
        //注冊(cè)監(jiān)聽(tīng)器
        [self.model addObserver:self forKeyPath:@"balance" options:NSKeyValueObservingOptionNew context:SubViewControllerBalanceObserverContext];
    }
    return self;
}

//監(jiān)聽(tīng)器回調(diào)方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    
    NSString *d = (__bridge NSString*)context;
    NSLog(@"SUB %@", d);
    if (context == SubViewControllerBalanceObserverContext)
    {
        NSLog(@"SubViewController NewBalance: %lf", self.model.balance);
    }
    
}

//刪除監(jiān)聽(tīng)器
- (void)dealloc
{
    [self.model removeObserver:self forKeyPath:@"balance" context:SubViewControllerBalanceObserverContext];
}

上述代碼運(yùn)行后瑞凑,根視圖控制器為ViewController展示一個(gè)按鈕,點(diǎn)擊后會(huì)創(chuàng)建SubViewController并展示概页,此時(shí)會(huì)有兩個(gè)按鈕籽御,一個(gè)退出、一個(gè)修改模型值,接下來(lái)點(diǎn)擊修改模型值按鈕會(huì)發(fā)現(xiàn)有如下輸出:

SUB CCCCCCCAAAA
SubViewController NewBalance: 8989.000000
SUB DDDDDDDD

這個(gè)結(jié)果是不是有點(diǎn)出乎意料技掏,當(dāng)我們點(diǎn)擊修改模型按鈕后會(huì)觸發(fā)監(jiān)聽(tīng)器的回調(diào)函數(shù)铃将,然后執(zhí)行SubViewController的回調(diào)方法就會(huì)輸出上面兩行的打印結(jié)果,那第三行是什么呢哑梳?第三行還是SubViewController的輸出結(jié)果劲阎,但是打印的context卻是DisplayViewController注冊(cè)的,這里我們就知道了涧衙,KVO在觸發(fā)回調(diào)函數(shù)時(shí)會(huì)向所有注冊(cè)了的監(jiān)聽(tīng)器發(fā)送回調(diào)信息哪工,也就是所有注冊(cè)了的監(jiān)聽(tīng)器都會(huì)執(zhí)行回調(diào)函數(shù),但由于繼承關(guān)系的存在沒(méi)有執(zhí)行父類(lèi)的回調(diào)函數(shù)而是執(zhí)行了兩次子類(lèi)的回調(diào)函數(shù)弧哎,因此雁比,為了使得父類(lèi)也能夠正確執(zhí)行監(jiān)聽(tīng)器的回調(diào)函數(shù),在子類(lèi)的回調(diào)函數(shù)中應(yīng)當(dāng)手動(dòng)調(diào)用撤嫩,所示子類(lèi)監(jiān)聽(tīng)器回調(diào)函數(shù)正確的寫(xiě)法應(yīng)是如下代碼:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if (context == SubViewControllerBalanceObserverContext)
    {
        NSLog(@"SubViewController NewBalance: %lf", self.model.balance);
    }
    else
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

當(dāng)context不屬于子類(lèi)定義時(shí)應(yīng)當(dāng)調(diào)用父類(lèi)的監(jiān)聽(tīng)器回調(diào)函數(shù)偎捎,其實(shí)這里還少了一個(gè)栗子,就是不使用context序攘,當(dāng)我們不使用context僅僅通過(guò)keyPath判斷茴她,根本無(wú)法得知繼承的父類(lèi)是否也在監(jiān)聽(tīng)同一對(duì)象,如果我們繼承的是第三方的框架程奠,很可能就會(huì)產(chǎn)生未知的異常丈牢。蘋(píng)果也建議我們針對(duì)我們監(jiān)聽(tīng)的每一個(gè)屬性都創(chuàng)建一個(gè)context,不建議使用keyPath來(lái)做字符串的判斷瞄沙,并且字符串判斷的效率也很低己沛,正確的context寫(xiě)法如下:

//靜態(tài)變量的地址可以保證context的獨(dú)一無(wú)二
static void * SubViewControllerBalanceObserverContext = &SubViewControllerBalanceObserverContext;

手動(dòng)觸發(fā)KVO

有時(shí)我們可能有一些需求,在屬性值滿足要求下才去觸發(fā)KVO距境,有的人可能會(huì)說(shuō)直接在回調(diào)函數(shù)中進(jìn)行判斷就好啦申尼,但是當(dāng)我們開(kāi)發(fā)一些供他人使用的框架時(shí)我們不能保證其他用戶能夠按照要求進(jìn)行條件判斷,此時(shí)就需要手動(dòng)觸發(fā)KVO垫桂。

觸發(fā)監(jiān)聽(tīng)器回調(diào)函數(shù)時(shí)需要滿足一個(gè)類(lèi)方法:

//balance屬性實(shí)現(xiàn)該方法
+ (BOOL)automaticallyNotifiesObserversOfBalance

//其他屬性按照以下格式實(shí)現(xiàn)類(lèi)方法
+ (BOOL)automaticallyNotifiesObserversOfXXXX

通過(guò)函數(shù)名就可以判斷师幕,該函數(shù)是用來(lái)判斷是否自行進(jìn)行監(jiān)聽(tīng)器通知,默認(rèn)返回true诬滩,因此默認(rèn)情況下都是自動(dòng)觸發(fā)KVO的回調(diào)函數(shù)霹粥,如果要手動(dòng)觸發(fā)則需要返回false并在需要觸發(fā)KVO回調(diào)函數(shù)的地方執(zhí)行以下方法:

    //對(duì)需要觸發(fā)回調(diào)函數(shù)的屬性名稱調(diào)用如下方法
    [self willChangeValueForKey:@"balance"];
    //為其賦新值
    _balance = balance;
    [self didChangeValueForKey:@"balance"];

舉個(gè)栗子如下:

#import <Foundation/Foundation.h>

@interface Account: NSObject

@property (nonatomic, copy) NSString *accountNumber;
@property (nonatomic, assign) double balance;

@end

@implementation Account

@synthesize accountNumber = _accountNumber;
@synthesize balance = _balance;

- (void)setBalance:(double)balance
{
    //如果新值小于0不觸發(fā)KVO
    if (balance < 0)
    {
        _balance = balance;
    }
    else
    {
        //新值大于0才觸發(fā)KVO回調(diào)函數(shù)
        [self willChangeValueForKey:@"balance"];
        _balance = balance;
        [self didChangeValueForKey:@"balance"];
    }
}

+ (BOOL)automaticallyNotifiesObserversOfBalance
{
    return NO;
}


@end

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, strong) Account *account;

- (void)setObserver;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;

- (void)setObserver
{
    [self.account addObserver:self forKeyPath:@"balance" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if (object == self.account && [keyPath isEqualToString:@"balance"])
    {
        NSLog(@"NewBalance: %lf", self.account.balance);
    }
}

- (void)dealloc
{
    [self.account removeObserver:self forKeyPath:@"balance" context:nil];
}

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p = [[Person alloc] init];
        
        p.account = [[Account alloc] init];
        p.account.balance = 100.0;
        [p setObserver];
        //執(zhí)行下面的代碼不會(huì)觸發(fā)KVO回調(diào)函數(shù)
        p.account.balance = -1000;
        //執(zhí)行下面這行代碼會(huì)輸出 NewBalance: 220.000000
        p.account.balance = 220.0;
    }
    return 0;
}

總結(jié)

通過(guò)上面一系列的例子可以發(fā)現(xiàn)KVO的坑挺多的,雖然基本的使用方法很簡(jiǎn)單碱呼,但是需要注意的地方也有很多蒙挑。正確的使用姿勢(shì)應(yīng)當(dāng)如下:

  • 使用靜態(tài)變量地址作為context,并且為每一個(gè)監(jiān)聽(tīng)的屬性都創(chuàng)建一個(gè)context愚臀,盡量不使用keyPath作為區(qū)分條件。
  • addObserverremoveObserver必須要成套出現(xiàn),建議在dealloc方法中刪除監(jiān)聽(tīng)器對(duì)象姑裂。
  • 如果有繼承關(guān)系馋袜,在監(jiān)聽(tīng)器回調(diào)函數(shù)中將不是當(dāng)前類(lèi)處理的context調(diào)用父類(lèi)的監(jiān)聽(tīng)器回調(diào)函數(shù)進(jìn)行處理。
  • 刪除監(jiān)聽(tīng)器時(shí)需要注意不要重復(fù)刪除舶斧,盡量使用context刪除卧波。

KVO底層實(shí)現(xiàn)

在官方文檔中有一點(diǎn)簡(jiǎn)介如下:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

關(guān)于isa指針于游、isa-swizzling本博客都有詳細(xì)介紹,有興趣的讀者可以自行查閱: iOS runtime探究(一): 從runtime開(kāi)始理解面向?qū)ο蟮念?lèi)到面向過(guò)程的結(jié)構(gòu)體

KVO的實(shí)現(xiàn)使用了isa-swizzling技術(shù)以及觀察者模式。
isa指針指向了對(duì)象的類(lèi)對(duì)象氮凝,這個(gè)類(lèi)對(duì)象維護(hù)著一個(gè)分發(fā)表,分發(fā)表保存了類(lèi)方法冰寻、成員方法實(shí)現(xiàn)的指針觉阅。

當(dāng)對(duì)一個(gè)對(duì)象的屬性第一次進(jìn)行監(jiān)聽(tīng)器注冊(cè)后,編譯器會(huì)默認(rèn)生成一個(gè)名稱為NSKVONotifying_原有類(lèi)名稱的派生中間類(lèi)嗜闻,該類(lèi)繼承原有類(lèi)蜕依,然后修改原有類(lèi)對(duì)象的isa指針,使其指向新生成的中間類(lèi)琉雳,接著样眠,會(huì)在派生類(lèi)中修改監(jiān)聽(tīng)屬性的settergetter方法,執(zhí)行willChangeValueForKey:didChangeValueForKey:方法和父類(lèi)的setter方法翠肘,并通知所有監(jiān)聽(tīng)的對(duì)象檐束,監(jiān)聽(tīng)屬性被修改了。

因此束倍,對(duì)于使用KVO監(jiān)聽(tīng)的類(lèi)來(lái)說(shuō)被丧,isa指針的指向并不一定指向?qū)ο蟮膶?shí)際類(lèi)。你不應(yīng)該依賴isa指針取決定類(lèi)的成員關(guān)系肌幽,而應(yīng)該使用class方法去正確的獲取對(duì)象的實(shí)際類(lèi)晚碾。

備注

由于作者水平有限,難免出現(xiàn)紕漏喂急,如有問(wèn)題還請(qǐng)不吝賜教格嘁。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市廊移,隨后出現(xiàn)的幾起案子糕簿,更是在濱河造成了極大的恐慌,老刑警劉巖狡孔,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件懂诗,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡苗膝,警方通過(guò)查閱死者的電腦和手機(jī)殃恒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人离唐,你說(shuō)我怎么就攤上這事病附。” “怎么了亥鬓?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵完沪,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我嵌戈,道長(zhǎng)覆积,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任熟呛,我火速辦了婚禮宽档,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惰拱。我一直安慰自己雌贱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般择诈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上降传,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音勾怒,去河邊找鬼婆排。 笑死,一個(gè)胖子當(dāng)著我的面吹牛笔链,可吹牛的內(nèi)容都是我干的段只。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鉴扫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼赞枕!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起坪创,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤炕婶,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后莱预,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體柠掂,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年依沮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涯贞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枪狂。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖肩狂,靈堂內(nèi)的尸體忽然破棺而出摘完,到底是詐尸還是另有隱情姥饰,我是刑警寧澤傻谁,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站列粪,受9級(jí)特大地震影響审磁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜岂座,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一态蒂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧费什,春花似錦钾恢、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至稿黍,卻和暖如春疹瘦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背巡球。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工言沐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人酣栈。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓险胰,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親矿筝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子起便,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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

  • 上半年有段時(shí)間做了一個(gè)項(xiàng)目,項(xiàng)目中聊天界面用到了音頻播放跋涣,涉及到進(jìn)度條缨睡,當(dāng)時(shí)做android時(shí)候處理的不太好,由于...
    DaZenD閱讀 3,017評(píng)論 0 26
  • *面試心聲:其實(shí)這些題本人都沒(méi)怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來(lái)就是把...
    Dove_iOS閱讀 27,139評(píng)論 30 470
  • 多線程陈辱、特別是NSOperation 和 GCD 的內(nèi)部原理奖年。運(yùn)行時(shí)機(jī)制的原理和運(yùn)用場(chǎng)景。SDWebImage的原...
    LZM輪回閱讀 2,007評(píng)論 0 12
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉沛贪,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,709評(píng)論 0 9
  • 重溫亦舒的《她比煙花寂寞》陋守,又有了不一樣的感悟震贵。 一個(gè)美麗奪目,演技精湛水评,驕傲自持的女明星猩系,姚晶,為了追求聚光燈下...
    柏青123閱讀 4,950評(píng)論 0 0