KVO(Key-Value Observing)
——鍵值觀察,它是一種機(jī)制湃鹊,它允許將其他對(duì)象的指定屬性的更改,通知給另一個(gè)對(duì)象镣奋。KVO蘋(píng)果文檔
關(guān)于KVO
如何創(chuàng)建使用币呵,大致分為三個(gè)步驟:
使用步驟
注冊(cè)觀察者
- 使用方法將觀察者注冊(cè)到觀察對(duì)象addObserver:forKeyPath:options:context:。
// 定義兩個(gè)上下文
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
options
是一個(gè)枚舉侨颈,包含了以下四個(gè)值:
NSKeyValueObservingOptionNew:
觀察更改后的值余赢;
NSKeyValueObservingOptionOld:
觀察更改前的值;
NSKeyValueObservingOptionInitial:
觀察最初的值(在注冊(cè)觀察服務(wù)時(shí)會(huì)調(diào)用一次觸發(fā)方法)哈垢;
NSKeyValueObservingOptionPrior:
分別在值修改前后觸發(fā)方法(即一次修改有兩次觸發(fā))
context
:上下文妻柒,包含任意數(shù)據(jù),這些數(shù)據(jù)將在相應(yīng)的更改通知中傳遞回觀察者耘分【偎可以指定NULL
并完全依賴(lài)KeyPath
字符串來(lái)確定更改通知的來(lái)源绑警,但是這種方法可能會(huì)導(dǎo)致對(duì)象的父類(lèi)由于不同的原因而觀察到相同的鍵路徑,從而導(dǎo)致問(wèn)題央渣。
一種更安全待秃,更可擴(kuò)展的方法是使用上下文確保您收到的通知是發(fā)給觀察者的,而不是超類(lèi)的痹屹。在類(lèi)中定義唯一命名的靜態(tài)變量的地址章郁,就滿足了良好的上下文條件。在父類(lèi)或子類(lèi)中以類(lèi)似方式選擇的上下文不太可能重疊志衍∨可以為整個(gè)類(lèi)選擇一個(gè)上下文,然后依靠通知消息中的KeyPath
字符串來(lái)確定更改的內(nèi)容楼肪。另外培廓,可以為每個(gè)觀察到的鍵路徑創(chuàng)建一個(gè)不同的上下文,從而完全不需要進(jìn)行字符串比較春叫,從而可以更有效地進(jìn)行通知解析肩钠。上面示例中顯示了以這種方式選擇的balance
和interestRate
屬性的示例上下文。
接受變更通知
- 在觀察者內(nèi)部實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:以接受更改通知消息暂殖。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
當(dāng)對(duì)象的觀察屬性的值更改時(shí)价匠,觀察者會(huì)收到一條observeValueForKeyPath:ofObject:change:context:
消息。所有觀察者都必須實(shí)現(xiàn)此方法呛每。
移除觀察者
- 當(dāng)觀察者不再應(yīng)接收消息時(shí)踩窖,使用該方法removeObserver:forKeyPath:注銷(xiāo)觀察者。至少在觀察者從內(nèi)存釋放之前調(diào)用注銷(xiāo)方法晨横,否則會(huì)導(dǎo)致奔潰洋腮。
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
典型的使用場(chǎng)景是在觀察者初始化期間(例如,在init
或viewDidLoad
)注冊(cè)為觀察者手形,在釋放過(guò)程中(通常在中dealloc)解除注冊(cè)啥供,以確保成對(duì)和有序地添加和刪除消息,并確保觀察者在從內(nèi)存中釋放之前被取消注冊(cè)库糠。
如果注冊(cè)了觀察者未注銷(xiāo)伙狐,當(dāng)再次進(jìn)入觀察者界面時(shí),會(huì)再次注冊(cè)KVO
觀察者曼玩,導(dǎo)致KVO觀察的重復(fù)注冊(cè)鳞骤,而第一次的通知對(duì)象還在內(nèi)存中,沒(méi)有進(jìn)行釋放黍判。如果此時(shí)接收到了屬性值變化的通知豫尽,會(huì)出現(xiàn)找不到原有的通知對(duì)象,只能找到現(xiàn)有的通知對(duì)象顷帖,即第二次KVO注冊(cè)的觀察者美旧,將會(huì)導(dǎo)致類(lèi)似野指針的崩潰渤滞,可理解為一直保持著一個(gè)野通知,且一直在監(jiān)聽(tīng)榴嗅。
問(wèn):多次添加注冊(cè)未注銷(xiāo)會(huì)不會(huì)造成循環(huán)引用妄呕?不會(huì),因?yàn)?code>observer在底層的字符串是weak修飾,所以不會(huì)導(dǎo)致循環(huán)引用嗽测。
自動(dòng) & 手動(dòng)變更通知
自動(dòng)變更通知
NSObject
提供自動(dòng)的鍵值更改通知的基本實(shí)現(xiàn)绪励。自動(dòng)鍵值更改通知將使用鍵值兼容訪問(wèn)器(setName
)以及鍵值編碼方法(setValue:forKey:
)進(jìn)行的更改通知給觀察者。由mutableArrayValueForKey:
返回的收集代理對(duì)象也支持自動(dòng)通知唠粥。
以下顯示的示例使該屬性的所有觀察者都name
收到有關(guān)更改的通知疏魏。
// 使用setter方法直接設(shè)置
[account setName:@"Savings"];
// 使用kvc設(shè)置name
[account setValue:@"Savings" forKey:@"name"];
// 使用keypath 設(shè)置document的name
[document setValue:@"Savings" forKeyPath:@"account.name"];
// 使用 mutableArrayValueForKey: to retrieve a relationship proxy object.
NSArray * arrayTrans = @{@"1001",@"1002"};
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject: arrayTrans];
手動(dòng)變更通知
這是切換手動(dòng)or自動(dòng)的方法,默認(rèn)YES
即為自動(dòng)變更通知晤愧,這里可以判斷theKey
來(lái)控制是否手動(dòng)變更通知大莫。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要實(shí)現(xiàn)手動(dòng)觀察者通知,在willChangeValueForKey:
更改值之前和didChangeValueForKey:
更改值之后調(diào)用官份。如下實(shí)現(xiàn)了該balance
屬性的手動(dòng)通知,首先檢查值是否已更改來(lái)最大程度地減少發(fā)送不必要的通知只厘。如下balance則可以實(shí)現(xiàn)僅在通知已更改時(shí)才提供通知。
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
如果一次操作多個(gè)更改時(shí)舅巷,就需要嵌套了多個(gè)鍵的更改通知羔味,如下
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
如果是有序的一對(duì)多關(guān)系,不僅必須指定已更改的鍵悄谐,還必須指定更改的類(lèi)型和所涉及對(duì)象的索引介评。NSKeyValueChange
類(lèi)型變化的有:NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
爬舰。受影響對(duì)象的索引作為NSIndexSet
對(duì)象傳遞。
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
// 刪除指定索引的事務(wù)對(duì)象寒瓦。
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}
觀察多屬性變化
注冊(cè)一個(gè)觀察者情屹,觀察多個(gè)屬性的變化。舉例:有一個(gè)我下載進(jìn)度杂腰,每次點(diǎn)擊屏幕觸發(fā)屬性值增加垃你,觀察currentData,totalData
的變化喂很,利用keyPathsForValuesAffectingValueForKey
通過(guò)keyPath
拼接的方式觀察兩個(gè)屬性值的變化惜颇,當(dāng)觀察到變化的值后,打印出變化后的值少辣。
//1凌摄、觀察一個(gè)數(shù)組 ,數(shù)組包含兩個(gè)屬性:currentData totalData
// ---Person.m---
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"currentData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
// 更改下載進(jìn)度數(shù)值
- (NSString *)downloadProgress{
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
// ----ViewController.m----
- (void)viewDidLoad {
[super viewDidLoad];
//2、注冊(cè)KVO觀察
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
}
//3漓帅、觸發(fā)屬性值增加
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.currentData += 10;
self.person.totalData += 1;
}
//4锨亏、收到變更通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
//4痴怨、移除觀察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"downloadProgress"];
}
觀察可變數(shù)組
觀察可變數(shù)組類(lèi)型,用到的是mutableArrayValueForKey
ormutableArrayValueForKeyPath
.
// 1器予、注冊(cè)可變數(shù)組KVO觀察者
- (void)viewDidLoad {
[super viewDidLoad];
self.account.dateArray = [NSMutableArray arrayWithCapacity:10];
[self.account addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}
// 2浪藻、接收變更通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
// 3、移除觀察者
- (void)dealloc{
[self.account removeObserver:self forKeyPath:@"dateArray"];
}
// 4乾翔、給數(shù)組添加數(shù)據(jù)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
KVO觀察屬性爱葵,不觀察成員變量
定義一個(gè)類(lèi),分別觀察其屬性name
和成員變量nickeName
的變化
self.account = [[Account alloc] init];
[self.account addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.account addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:NULL];
觸發(fā)對(duì)屬性和成員變量的賦值反浓,執(zhí)行結(jié)果如下:
KVO在對(duì)屬性萌丈、成員變量觀察時(shí),只觀察到了屬性的變化勾习。原因是屬性比成員變量多
setter
方法浓瞪,而KVO觀察的就是setter
方法。
中間類(lèi)
蘋(píng)果關(guān)于KVO實(shí)現(xiàn)的的解釋
- 自動(dòng)鍵值觀察是使用是基于isa-swizzling(指針裝換)的技術(shù)實(shí)現(xiàn)的巧婶。
- 該
isa
指針乾颁,顧名思義,指向?qū)ο蟮念?lèi)艺栈,它保持一個(gè)調(diào)度表英岭。該調(diào)度表實(shí)質(zhì)上包含指向該類(lèi)實(shí)現(xiàn)的方法的指針以及其他數(shù)據(jù)。- 在為對(duì)象的屬性注冊(cè)觀察者時(shí)湿右,將修改觀察對(duì)象的
isa
指針诅妹,它指向了中間類(lèi)而不是真實(shí)類(lèi)。因此毅人,isa指針的值不一定反映實(shí)例的實(shí)際類(lèi)吭狡。- 不要依靠
isa
指針來(lái)確定類(lèi)的成員變量。相反丈莺,應(yīng)該使用該類(lèi)方法確定對(duì)象實(shí)例的類(lèi)划煮。
中間類(lèi)的產(chǎn)生
根據(jù)這段文字描述,isa
指針在為對(duì)象的屬性注冊(cè)觀察者時(shí)缔俄,觀察對(duì)象的isa
指針會(huì)發(fā)生改邊弛秋,指向了一個(gè)中間類(lèi)±兀可以做一個(gè)簡(jiǎn)單的探究蟹略。
以觀察剛才的account
對(duì)象屬性name
為例,探究在添加觀察者之后類(lèi)是否發(fā)生了變化
由上面的結(jié)果知道遏佣,在注冊(cè)觀察者之前挖炬,對(duì)象的類(lèi)是
Account
,注冊(cè)之后贼急,實(shí)例對(duì)象的指針地址發(fā)生了變化茅茂,指向了一個(gè)中間類(lèi)NSKVONotifying_Account
捏萍。
中間類(lèi)是否為子類(lèi)?
不禁好奇這個(gè)NSKVONotifying_Account
中間類(lèi)是怎樣一個(gè)存在空闲,是否為Account的子類(lèi)呢令杈?可以試著打印account
對(duì)象的類(lèi)探究下
// ---------------
self.account = [[Account alloc] init];
[self printClasses:self.account.class];
[self.account addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self printClasses:self.account.class];
// ---------------
#pragma mark - 遍歷類(lèi)以及子類(lèi)
- (void)printClasses:(Class)cls{
// 注冊(cè)類(lèi)的總數(shù)
int count = objc_getClassList(NULL, 0);
// 創(chuàng)建一個(gè)數(shù)組, 其中包含給定對(duì)象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 獲取所有已注冊(cè)的類(lèi)
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[I]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
打印之
根據(jù)結(jié)果可以知道:
NSKVONotifying_Account
中間類(lèi)是Account
的子類(lèi)碴倾。
中間類(lèi)的方法
接下來(lái)逗噩,順藤摸瓜,可以探究下中間類(lèi)中有什么方法跌榔?
定義一個(gè)打印方法列表的方法
#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[I];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
同理再次執(zhí)行异雁,分析一下打印結(jié)果
中間類(lèi)打印出四個(gè)方法:
setName、class僧须、dealloc纲刀、_isKVOA
,為了方便研究是什么担平,在Acount類(lèi)中添加兩個(gè)實(shí)例方法:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Account : NSObject
{
NSString * nickName;
}
@property (nonatomic,copy) NSString * name;
-(void)method1;
-(void)method2;
@end
NS_ASSUME_NONNULL_END
為account
添加一個(gè)子類(lèi)SubAccount
,只重寫(xiě)父類(lèi)name
的setter
方法示绊,其他只集成;
#import "SubAccount.h"
@implementation SubAccount
// 重寫(xiě)父類(lèi)是setName方法
- (void)setName:(NSString *)name
{
}
@end
再執(zhí)行分析結(jié)果
根據(jù)以上可以知道,
SubAccount
集成了Account
類(lèi),并且重寫(xiě)了setName
方法暂论,在遍歷子類(lèi)方法列表時(shí)面褐,只打印出了setName
方法。說(shuō)明NSKVONotifying_Account
是重寫(xiě)了父類(lèi)Account
的四個(gè)方法取胎,這四個(gè)方法是否真的是Account
的呢展哭?為了研究這個(gè),我們可以打印出Account目前的方法列表
由上面的結(jié)果可以知道闻蛀,
NSKVONotifying_Account
只重寫(xiě)了setName
的方法匪傍,剩下的class、dealloc觉痛、_isKVOA
則是重寫(xiě)了Account
父類(lèi)NSObject
的方法析恢。
isa指針重指向觀察者類(lèi)
經(jīng)過(guò)探究,在移除觀察者時(shí)秧饮,isa指針重指向了Account
類(lèi)
在調(diào)用注銷(xiāo)方法之后,觀察者對(duì)象的中間類(lèi)消失泽篮,實(shí)例對(duì)象的類(lèi)還原成
Account
盗尸,那這個(gè)中間類(lèi)是否是真的消失了?
在ViewController
頁(yè)面我們介入一個(gè)打印Account類(lèi)及子類(lèi)的方法帽撑,第一次進(jìn)入程序時(shí)泼各,打印它,之后進(jìn)入測(cè)試KVO界面亏拉,打印出注冊(cè)觀察者前后Account
的類(lèi)變化,最后注銷(xiāo)觀察者前后的Account
的類(lèi)變化扣蜻,再次回到ViewController
頁(yè)面逆巍,再次打印之。
可以知道莽使,中間類(lèi)
NSKVONotifying_Account
并未隨著KVO界面消息锐极、注銷(xiāo)觀察者之后就消失,它依然還是作為Account
的子類(lèi)芳肌,在其開(kāi)辟的內(nèi)存空間里面灵再,這樣的目的就是為了復(fù)用
。
總結(jié)
- 給實(shí)例對(duì)象注冊(cè)KVO觀察者后亿笤,實(shí)例對(duì)象的指針地址會(huì)發(fā)生變化,會(huì)生成一個(gè)實(shí)例對(duì)象父類(lèi)的一個(gè)子類(lèi)
NSKVONotifying_xxxx
; - 中間類(lèi)方法會(huì)重寫(xiě)父類(lèi)的方法翎迁;其中
class、dealloc净薛、_isKVOA
則是重寫(xiě)了基類(lèi)NSObject
的方法汪榔。(dealloc
:重寫(xiě)了釋放方法、_isKVOA
:判斷是否為KVO類(lèi)) - 移除觀察者后肃拜,觀察者類(lèi)會(huì)還原為初始化類(lèi)(isa指針重指向初始化類(lèi))痴腌;但中間類(lèi)并未從內(nèi)存中移除,目的是為了方便復(fù)用爆班。