你要知道的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)行講解,讀者可按需查閱妆兑。
- KVC 使用方法詳解及底層實(shí)現(xiàn)
- KVO 正確使用姿勢(shì)進(jìn)階及底層實(shí)現(xiàn)
- Protocol與Delegate 使用方法詳解
- NSNotificationCenter 通知使用方法詳解
- KVO魂拦、Delegate、Notification 區(qū)別及相關(guān)使用場(chǎng)景
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)視圖芯勘。能夠上述需求的方法有很多,后面要講的Delegate
和Notification
都可以實(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í)刻保證addObserver
和removeObserver
成對(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è)面如下:
該視圖只有兩個(gè)按鈕陆淀,Click Me
為第一個(gè)按鈕,點(diǎn)擊后觸發(fā)buttonClicked
方法先嬉,該方法創(chuàng)建DisplayViewController
后直接展示出來(lái)轧苫,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)我們把DisplayViewController
的dealloc
方法去掉注釋后一切運(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í)傳入的context
為nil
,可能很多初學(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è)ViewController
和DisplayViewController
都監(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ū)分條件。 -
addObserver
與removeObserver
必須要成套出現(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)屬性的setter
和getter
方法,執(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)不吝賜教格嘁。