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

你要知道的KVC、KVO、Delegate、Notification都在這里

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

本系列文章主要通過講解KVC、KVO、Delegate、Notification的使用方法,來探討KVO、Delegate、Notification的區(qū)別以及相關(guān)使用場景,本系列文章將分一下幾篇文章進(jìn)行講解,讀者可按需查閱。

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

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

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

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

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

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

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

/*
刪除監(jiān)聽器
監(jiān)聽器對象為observer,被監(jiān)聽對象為消息的發(fā)送者即方法的調(diào)用者,應(yīng)與addObserver方法匹配
監(jiān)聽的屬性路徑為keyPath,應(yīng)與addObserver方法的keyPath匹配
監(jiān)聽上下文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è)方法,該方法由于沒有傳遞context可能會(huì)產(chǎn)生異常結(jié)果
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

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

舉一個(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;

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

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

//Person銷毀時(shí)調(diào)用的方法
- (void)dealloc
{
    /*
    切記,當(dāng)我們添加監(jiān)聽器時(shí)一定要在對象被銷毀前刪除該監(jiān)聽器
    刪除監(jiān)聽器傳遞的參數(shù)要與添加監(jiān)聽器傳參一致
    監(jiān)聽器也不可以重復(fù)刪除,如果沒有注冊監(jiān)聽器而去執(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)聽器
        [p setObserver];
        //重新對account的balance賦值后會(huì)觸發(fā)回調(diào)函數(shù)
        //輸出: NewBalance: 200.0
        p.account.balance = 200.0;        
    }
    return 0;
}

上面的例子很簡單,運(yùn)行結(jié)果也很正常,在Person類對象被銷毀前也進(jìn)行了監(jiān)聽器的刪除操作,并且運(yùn)行結(jié)果也很正常,相信很多人在實(shí)際的開發(fā)過程中也都是按照這樣方式實(shí)現(xiàn)的KVO,不幸的是,上面的寫法有很多缺陷。

首先,講解一下為什么要在對象被銷毀前刪除監(jiān)聽器,我們在開發(fā)中使用KVO時(shí)很可能會(huì)遇到因?yàn)闆]有刪除監(jiān)聽器而產(chǎn)生的野指針錯(cuò)誤。

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

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

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

//第一個(gè)按鈕點(diǎn)擊處理器
- (void)buttonClicked
{
    /*
    另一個(gè)UIViewController為DisplayViewController
    在開發(fā)中經(jīng)常會(huì)遇到這樣的情形,需要?jiǎng)?chuàng)建一個(gè)VC來展示Model的數(shù)據(jù)
    以下兩行代碼就是用來創(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;
}


/*
接下來實(shí)現(xiàn)DisplayViewController
假設(shè)DisplayViewController中需要對Model進(jìn)行進(jìn)一步處理,所以需要監(jiān)聽Model的balance屬性,并在initWithModel:初始化方法中添加監(jiān)聽器
*/
//初始化方法,添加一個(gè)退出按鈕,并添加model的balance屬性監(jiān)聽器
- (instancetype)initWithModel:(Model*)model;
{
    if (self = [super init])
    {
        self.view.backgroundColor = [UIColor whiteColor];
        
        self.model = model;
        
        //創(chuàng)建監(jiān)聽器
        [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)前頁面
    [self dismissViewControllerAnimated:YES completion:nil];
}

//監(jiān)聽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方法注釋,因此當(dāng)DisplayViewController銷毀時(shí)不會(huì)刪除監(jiān)聽器
//- (void)dealloc
//{
//    [self.model removeObserver:self forKeyPath:@"balance" context:nil];
//}

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

ViewController頁面

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

DisplayViewController頁面

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

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

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

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

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

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

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

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

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

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

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

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

DisplayViewController還是之前栗子的

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

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

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

//在初始化方法中輸入上面的變量作為context進(jìn)行監(jiān)聽器的注冊
[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)聽器回調(diào)函數(shù)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    //將void *的context轉(zhuǎn)換為NSString類型
    NSString *d = (__bridge NSString*)context;
    NSLog(@"DIS %@", d);
    
    if (context == DisplayViewControllerBalanceObserverContext)
    {
        NSLog(@"DDD New Balance %lf", self.model.balance);
    }
}

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

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

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

//監(jiān)聽器回調(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)聽器
- (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è)修改模型值,接下來點(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)聽器的回調(diào)函數(shù),然后執(zhí)行SubViewController的回調(diào)方法就會(huì)輸出上面兩行的打印結(jié)果,那第三行是什么呢?第三行還是SubViewController的輸出結(jié)果,但是打印的context卻是DisplayViewController注冊的,這里我們就知道了,KVO在觸發(fā)回調(diào)函數(shù)時(shí)會(huì)向所有注冊了的監(jiān)聽器發(fā)送回調(diào)信息,也就是所有注冊了的監(jiān)聽器都會(huì)執(zhí)行回調(diào)函數(shù),但由于繼承關(guān)系的存在沒有執(zhí)行父類的回調(diào)函數(shù)而是執(zhí)行了兩次子類的回調(diào)函數(shù),因此,為了使得父類也能夠正確執(zhí)行監(jiān)聽器的回調(diào)函數(shù),在子類的回調(diào)函數(shù)中應(yīng)當(dāng)手動(dòng)調(diào)用,所示子類監(jiān)聽器回調(diào)函數(shù)正確的寫法應(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不屬于子類定義時(shí)應(yīng)當(dāng)調(diào)用父類的監(jiān)聽器回調(diào)函數(shù),其實(shí)這里還少了一個(gè)栗子,就是不使用context,當(dāng)我們不使用context僅僅通過keyPath判斷,根本無法得知繼承的父類是否也在監(jiān)聽同一對象,如果我們繼承的是第三方的框架,很可能就會(huì)產(chǎn)生未知的異常。蘋果也建議我們針對我們監(jiān)聽的每一個(gè)屬性都創(chuàng)建一個(gè)context,不建議使用keyPath來做字符串的判斷,并且字符串判斷的效率也很低,正確的context寫法如下:

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

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

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

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

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

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

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

    //對需要觸發(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é)

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

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

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

在官方文檔中有一點(diǎ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開始理解面向?qū)ο蟮念惖矫嫦蜻^程的結(jié)構(gòu)體

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

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

因此,對于使用KVO監(jiān)聽的類來說,isa指針的指向并不一定指向?qū)ο蟮膶?shí)際類。你不應(yīng)該依賴isa指針取決定類的成員關(guān)系,而應(yīng)該使用class方法去正確的獲取對象的實(shí)際類。

備注

由于作者水平有限,難免出現(xiàn)紕漏,如有問題還請不吝賜教。

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

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

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