你要知道的KVC、KVO、Delegate、Notification都在這里
轉(zhuǎn)載請注明出處 http://www.itdecent.cn/p/d3bfa1e9fa0a
本系列文章主要通過講解KVC、KVO、Delegate、Notification的使用方法,來探討KVO、Delegate、Notification的區(qū)別以及相關(guān)使用場景,本系列文章將分一下幾篇文章進(jìn)行講解,讀者可按需查閱。
- KVC 使用方法詳解及底層實現(xiàn)
- KVO 正確使用姿勢進(jìn)階及底層實現(xiàn)
- Protocol與Delegate 使用方法詳解
- NSNotificationCenter 通知使用方法詳解
- KVO、Delegate、Notification 區(qū)別及相關(guān)使用場景
KVO 正確使用姿勢進(jìn)階及底層實現(xiàn)
KVO(key value observing)鍵值監(jiān)聽是我們在開發(fā)中常使用的用于監(jiān)聽特定對象屬性值變化的方法,常用于監(jiān)聽數(shù)據(jù)模型的變化從而可以動態(tài)的修改對應(yīng)視圖。能夠上述需求的方法有很多,后面要講的Delegate和Notification都可以實現(xiàn),但都有各自的優(yōu)缺點和適用場景,需要根據(jù)實際情況按需選擇,但三者都很重要,在開發(fā)中都會使用。
與KVC相同,OC在實現(xiàn)KVO時沒有采用實現(xiàn)接口的方式,而是針對NSObject創(chuàng)建了一個類別,通過這樣的方式使得NSObject的子類可以自行實現(xiàn)NSKeyValueObserving類別定義的相關(guān)方法,其他的如NSArray、NSSet這樣的集合類也都定義了相關(guān)的類別,因此也可以對集合類型進(jìn)行KVO的監(jiān)聽。本文主要進(jìn)行KVO進(jìn)階講解,基礎(chǔ)知識還需讀者自行查閱。
學(xué)習(xí)KVO最好的方法就是閱讀官方文檔:Key-Value Observing Programming Guide
KVO基礎(chǔ)方法詳解進(jìn)階
KVO常用的方法有如下幾個:
/*
注冊監(jiān)聽器
監(jiān)聽器對象為observer,被監(jiān)聽對象為消息的發(fā)送者即方法的調(diào)用者在回調(diào)函數(shù)中會被回傳
監(jiān)聽的屬性路徑為keyPath支持點語法的嵌套
監(jiān)聽類型為options支持按位或來監(jiān)聽多個事件類型
監(jiān)聽上下文context主要用于在多個監(jiān)聽器對象監(jiān)聽相同keyPath時進(jìn)行區(qū)分
添加監(jiān)聽器只會保留監(jiān)聽器對象的地址,不會增加引用,也不會在對象釋放后置空,因此需要自己持有監(jiān)聽對象的強引用,該參數(shù)也會在回調(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));
/*
與上一個方法相同,只是少了context參數(shù)
推薦使用上一個方法,該方法由于沒有傳遞context可能會產(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;
舉一個簡單的栗子:
#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有一個對象是bank可以監(jiān)聽bank是否營業(yè),可以寫"bank.isOpen"
監(jiān)聽上下文設(shè)置為nil,相信很多人在使用的時候都會這么寫
*/
[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銷毀時調(diào)用的方法
- (void)dealloc
{
/*
切記,當(dāng)我們添加監(jiān)聽器時一定要在對象被銷毀前刪除該監(jiān)聽器
刪除監(jiān)聽器傳遞的參數(shù)要與添加監(jiān)聽器傳參一致
監(jiān)聽器也不可以重復(fù)刪除,如果沒有注冊監(jiān)聽器而去執(zhí)行刪除操作也會拋出異常
*/
[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賦值后會觸發(fā)回調(diào)函數(shù)
//輸出: NewBalance: 200.0
p.account.balance = 200.0;
}
return 0;
}
上面的例子很簡單,運行結(jié)果也很正常,在Person類對象被銷毀前也進(jìn)行了監(jiān)聽器的刪除操作,并且運行結(jié)果也很正常,相信很多人在實際的開發(fā)過程中也都是按照這樣方式實現(xiàn)的KVO,不幸的是,上面的寫法有很多缺陷。
首先,講解一下為什么要在對象被銷毀前刪除監(jiān)聽器,我們在開發(fā)中使用KVO時很可能會遇到因為沒有刪除監(jiān)聽器而產(chǎn)生的野指針錯誤。
KVO在注冊監(jiān)聽器的時候不會持有監(jiān)聽器對象的引用,也不會像weak那樣在監(jiān)聽器對象被銷毀時置nil,而是僅僅保留監(jiān)聽器對象的地址,類似于copy修飾符,當(dāng)監(jiān)聽器對象被銷毀而又沒有刪除監(jiān)聽器時,如果這個時候被監(jiān)聽對象的值發(fā)生變化系統(tǒng)會執(zhí)行監(jiān)聽器的回調(diào)函數(shù),這個時候監(jiān)聽器對象已經(jīng)不存在了,KVO保留的地址就是一個野指針,因此會產(chǎn)生野指針錯誤。上面的栗子由于在對象被銷毀前沒有修改account.balance的值,因此哪怕不刪除監(jiān)聽器也不會產(chǎn)生野指針異常,但我們需要注意的是,要時刻保證addObserver和removeObserver成對出現(xiàn),避免野指針錯誤的產(chǎn)生。
接下來舉一個會產(chǎn)生野指針異常的栗子:
/*
首先實現(xiàn)兩個UIViewController
以下代碼為ViewController代碼,在ViewController中添加兩個按鈕,并分別添加兩個點擊事件。其他代碼不再展示,讀者可自行完善
*/
//第一個按鈕點擊處理器
- (void)buttonClicked
{
/*
另一個UIViewController為DisplayViewController
在開發(fā)中經(jīng)常會遇到這樣的情形,需要創(chuàng)建一個VC來展示Model的數(shù)據(jù)
以下兩行代碼就是用來創(chuàng)建并展示該VC
*/
DisplayViewController *vc = [[DisplayViewController alloc] initWithModel:self.model];
[self presentViewController:vc animated:YES completion:nil];
}
//第二個按鈕點擊處理器
- (void)button2Clicked
{
//模擬模型數(shù)據(jù)發(fā)生變化
self.model.balance = 8888;
}
/*
接下來實現(xiàn)DisplayViewController
假設(shè)DisplayViewController中需要對Model進(jìn)行進(jìn)一步處理,所以需要監(jiān)聽Model的balance屬性,并在initWithModel:初始化方法中添加監(jiān)聽器
*/
//初始化方法,添加一個退出按鈕,并添加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銷毀時不會刪除監(jiān)聽器
//- (void)dealloc
//{
// [self.model removeObserver:self forKeyPath:@"balance" context:nil];
//}
上述代碼完成后,運行程序,ViewController頁面如下:

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

該視圖只有一個按鈕,點擊黑色按鈕后退出頁面,回到ViewController視圖中,此時并沒有任何錯誤產(chǎn)生,盡管我們在DisplayViewController銷毀后也沒有刪除其監(jiān)聽器,這個邏輯在開發(fā)中經(jīng)常遇到,在一個頁面獲取到數(shù)據(jù)后使用另一個頁面來展示相關(guān)數(shù)據(jù),另一個頁面很有可能會根據(jù)需求來監(jiān)聽模型對象。此時如果點擊第二個按鈕BTN2不幸的事情就會產(chǎn)生,在button2Clicked方法中會產(chǎn)生野指針錯誤,因為在該方法中修改了model.balance的值,由于前一個視圖中沒有刪除監(jiān)聽器,KVO中仍然有監(jiān)聽器的存在,此時會觸發(fā)監(jiān)聽器的回調(diào)方法,但DisplayViewController早已銷毀,因此產(chǎn)生野指針錯誤,當(dāng)我們把DisplayViewController的dealloc方法去掉注釋后一切運行正常,因為在DisplayViewController銷毀時也刪除了監(jiān)聽器。
上面這個栗子產(chǎn)生的野指針錯誤正是因為KVO使用不正確,可能有些讀者沒有在監(jiān)聽器銷毀前刪除監(jiān)聽器也沒有發(fā)生過任何異常,因此不太注意,但KVO正確使用姿勢一定是在監(jiān)聽器對象銷毀前刪除監(jiān)聽器。
上面的例子看似解決了一個問題,需要注意的是上面的栗子在創(chuàng)建監(jiān)聽器時傳入的context為nil,可能很多初學(xué)者都會這么寫,接下來繼續(xù)看一個栗子:
/*
本示例與上一個栗子相同,只是在ViewController中注冊了model.balance的監(jiān)聽器
*/
//ViewController.m
//在初始化時注冊model.balance監(jiān)聽器
/*
DisplayViewController與上一個栗子一樣,但多添加一個按鈕
*/
- (void)changeValueButtonClickedHandler
{
self.model.balance = 8989;
}
上面這個栗子與前一個類似,只不過在ViewController中同樣添加了對model.balance的監(jiān)聽,也就是說兩個ViewController和DisplayViewController都監(jiān)聽了同一個對象的屬性值,這在開發(fā)中也很常見,在DisplayViewController中添加了一個按鈕用于模擬在DisplayViewController中修改model.balance值的操作,現(xiàn)在兩個視圖都監(jiān)聽了同一對象的屬性值,那當(dāng)我們展示DisplayViewController后修改了model.balance的值,此時會觸發(fā)哪個視圖的回調(diào)函數(shù)呢?實驗一下就能發(fā)現(xiàn)兩個視圖的監(jiān)聽器回調(diào)函數(shù)都觸發(fā)了。
但KVO還有一個可能會產(chǎn)生錯誤的地方,在看下一個栗子之前有一點需要說明,有時候我們可能在一個視圖中監(jiān)聽很多模型對象,當(dāng)然了可以按照我們常用的通過keyPath字符串來判斷產(chǎn)生回調(diào)的具體是哪個屬性值,但如果監(jiān)聽很多屬性值,這樣的方法似乎看起來很凌亂,而且逐一進(jìn)行字符串判斷感覺很浪費資源,并且當(dāng)我們在后期修改了屬性的名稱還不能忘記修改監(jiān)聽器的keyPath判斷語句,那有什么辦法能夠取代keyPath嗎?答案是context,初學(xué)者經(jīng)常直接將context置為nil,但context才是KVO保證正確運行的關(guān)鍵。
context是一個id類型的參數(shù),在注冊監(jiān)聽器時可以傳入該參數(shù),在回調(diào)函數(shù)中會回傳該參數(shù),因此,該參數(shù)就能完美的解決上述兩個問題。那context這個id類型的參數(shù)設(shè)置為什么值比較合適呢?可能第一感覺還是設(shè)置為NSString類型,但這樣仍然可能會產(chǎn)生沖突,蘋果推薦的做法是創(chuàng)建一個靜態(tài)變量然后使用該靜態(tài)變量的地址作為context,通過這樣的方法就能夠保證context的獨一無二。
接下來看下一個栗子:
/*
本栗子需要使用三個UIViewController
ViewController根視圖控制器
DisplayViewController 父視圖控制器
SubViewController 子視圖控制器
ViewController不監(jiān)聽模型,包括一個按鈕用于創(chuàng)建SubViewController并展示
DisplayViewController還是之前栗子的
SubViewController繼承DisplayViewController并且也創(chuàng)建了監(jiān)聽器來監(jiān)聽model.balance屬性
*/
//ViewController部分代碼如下
//該控制器只有一個按鈕
- (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];
}
上述代碼運行后,根視圖控制器為ViewController展示一個按鈕,點擊后會創(chuàng)建SubViewController并展示,此時會有兩個按鈕,一個退出、一個修改模型值,接下來點擊修改模型值按鈕會發(fā)現(xiàn)有如下輸出:
SUB CCCCCCCAAAA
SubViewController NewBalance: 8989.000000
SUB DDDDDDDD
這個結(jié)果是不是有點出乎意料,當(dāng)我們點擊修改模型按鈕后會觸發(fā)監(jiān)聽器的回調(diào)函數(shù),然后執(zhí)行SubViewController的回調(diào)方法就會輸出上面兩行的打印結(jié)果,那第三行是什么呢?第三行還是SubViewController的輸出結(jié)果,但是打印的context卻是DisplayViewController注冊的,這里我們就知道了,KVO在觸發(fā)回調(diào)函數(shù)時會向所有注冊了的監(jiān)聽器發(fā)送回調(diào)信息,也就是所有注冊了的監(jiān)聽器都會執(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)手動調(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不屬于子類定義時應(yīng)當(dāng)調(diào)用父類的監(jiān)聽器回調(diào)函數(shù),其實這里還少了一個栗子,就是不使用context,當(dāng)我們不使用context僅僅通過keyPath判斷,根本無法得知繼承的父類是否也在監(jiān)聽同一對象,如果我們繼承的是第三方的框架,很可能就會產(chǎn)生未知的異常。蘋果也建議我們針對我們監(jiān)聽的每一個屬性都創(chuàng)建一個context,不建議使用keyPath來做字符串的判斷,并且字符串判斷的效率也很低,正確的context寫法如下:
//靜態(tài)變量的地址可以保證context的獨一無二
static void * SubViewControllerBalanceObserverContext = &SubViewControllerBalanceObserverContext;
手動觸發(fā)KVO
有時我們可能有一些需求,在屬性值滿足要求下才去觸發(fā)KVO,有的人可能會說直接在回調(diào)函數(shù)中進(jìn)行判斷就好啦,但是當(dāng)我們開發(fā)一些供他人使用的框架時我們不能保證其他用戶能夠按照要求進(jìn)行條件判斷,此時就需要手動觸發(fā)KVO。
觸發(fā)監(jiān)聽器回調(diào)函數(shù)時需要滿足一個類方法:
//balance屬性實現(xiàn)該方法
+ (BOOL)automaticallyNotifiesObserversOfBalance
//其他屬性按照以下格式實現(xiàn)類方法
+ (BOOL)automaticallyNotifiesObserversOfXXXX
通過函數(shù)名就可以判斷,該函數(shù)是用來判斷是否自行進(jìn)行監(jiān)聽器通知,默認(rèn)返回true,因此默認(rèn)情況下都是自動觸發(fā)KVO的回調(diào)函數(shù),如果要手動觸發(fā)則需要返回false并在需要觸發(fā)KVO回調(diào)函數(shù)的地方執(zhí)行以下方法:
//對需要觸發(fā)回調(diào)函數(shù)的屬性名稱調(diào)用如下方法
[self willChangeValueForKey:@"balance"];
//為其賦新值
_balance = balance;
[self didChangeValueForKey:@"balance"];
舉個栗子如下:
#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í)行下面的代碼不會觸發(fā)KVO回調(diào)函數(shù)
p.account.balance = -1000;
//執(zhí)行下面這行代碼會輸出 NewBalance: 220.000000
p.account.balance = 220.0;
}
return 0;
}
總結(jié)
通過上面一系列的例子可以發(fā)現(xiàn)KVO的坑挺多的,雖然基本的使用方法很簡單,但是需要注意的地方也有很多。正確的使用姿勢應(yīng)當(dāng)如下:
- 使用靜態(tài)變量地址作為
context,并且為每一個監(jiān)聽的屬性都創(chuàng)建一個context,盡量不使用keyPath作為區(qū)分條件。 -
addObserver與removeObserver必須要成套出現(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)聽器時需要注意不要重復(fù)刪除,盡量使用
context刪除。
KVO底層實現(xià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的實現(xiàn)使用了isa-swizzling技術(shù)以及觀察者模式。
isa指針指向了對象的類對象,這個類對象維護(hù)著一個分發(fā)表,分發(fā)表保存了類方法、成員方法實現(xiàn)的指針。
當(dāng)對一個對象的屬性第一次進(jìn)行監(jiān)聽器注冊后,編譯器會默認(rèn)生成一個名稱為NSKVONotifying_原有類名稱的派生中間類,該類繼承原有類,然后修改原有類對象的isa指針,使其指向新生成的中間類,接著,會在派生類中修改監(jiān)聽屬性的setter和getter方法,執(zhí)行willChangeValueForKey:和didChangeValueForKey:方法和父類的setter方法,并通知所有監(jiān)聽的對象,監(jiān)聽屬性被修改了。
因此,對于使用KVO監(jiān)聽的類來說,isa指針的指向并不一定指向?qū)ο蟮膶嶋H類。你不應(yīng)該依賴isa指針取決定類的成員關(guān)系,而應(yīng)該使用class方法去正確的獲取對象的實際類。
備注
由于作者水平有限,難免出現(xiàn)紕漏,如有問題還請不吝賜教。