什么是KVO擂达?
KVO
是一種機(jī)制,他是建立在KVC的基礎(chǔ)上的胶滋,他可以將其他對(duì)象屬性值的變化通知給對(duì)象板鬓。
1.1、注冊(cè)KVO
您必須執(zhí)行以下步驟究恤,才能使對(duì)象能夠接收KVO兼容屬性的鍵值觀察通知:
- 使用方法
addObserver:forKeyPath:options:context:
將觀察者注冊(cè)到觀察對(duì)象俭令。 -
observeValueForKeyPath:ofObject:change:context:
在觀察者內(nèi)部實(shí)現(xiàn)這個(gè)方法以接收更改通知消息。 -
removeObserver:forKeyPath:
當(dāng)觀察者不再需要接收消息時(shí)部宿,使用該方法注銷觀察者抄腔。最晚在從內(nèi)存釋放觀察者之前調(diào)用此方法。 -
removeObserver:forKeyPath:context:
當(dāng)我們?cè)谧?cè)觀察者的時(shí)候理张,如果context
參數(shù)不為NULL
時(shí)赫蛇,應(yīng)該使用這個(gè)方法來(lái)移除,這樣更安全雾叭。
1.2悟耘、context參數(shù)解釋
addObserver:forKeyPath:options:context:
方法中的context
參數(shù)將在相應(yīng)的observeValueForKeyPath:ofObject:change:context:
中回傳給觀察者。你可以將這個(gè)參數(shù)指定為NULL
织狐,通過(guò)依賴keyPath
來(lái)確定觀察屬性的來(lái)源暂幼,但是當(dāng)有多個(gè)對(duì)象具有相同的屬性被觀察時(shí)掘殴,根據(jù)keyPath
來(lái)判斷就顯得不那么方便了。
一種更安全粟誓,更具擴(kuò)展性的方法是使用context
來(lái)進(jìn)行區(qū)分奏寨。
context
指針的創(chuàng)建。
static void * PersonAccountBalanceContext =&PersonAccountBalanceContext;
static void * PersonAccountInterestRateContext =&PersonAccountInterestRateContext;
2.1鹰服、接收KVO的通知
當(dāng)觀察到的對(duì)象屬性值改變時(shí)病瞳,觀察者會(huì)收到一條observeValueForKeyPath:ofObject:change:context:
消息。所有觀察者都必須實(shí)現(xiàn)此方法悲酷。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something
} else if (context == PersonAccountInterestRateContext) {
// Do something
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
當(dāng)我們?cè)谧?cè)觀察者的時(shí)候使用context
參數(shù)時(shí)套菜,那么在接收通知的地方就可以使用context
來(lái)區(qū)分是哪個(gè)對(duì)象的屬性觸發(fā)了通知回調(diào)。
如果在注冊(cè)觀察者時(shí)用NULL
傳遞個(gè)context
设易,那么將使用keyPath
來(lái)進(jìn)行比較逗柴,已確定是哪個(gè)對(duì)象的屬性進(jìn)行了更改。
無(wú)論如何顿肺,觀察者應(yīng)始終observeValueForKeyPath:ofObject:change:context:
在其無(wú)法識(shí)別context
(或在簡(jiǎn)單情況下戏溺,是任意的keyPath
)時(shí)調(diào)用父類的實(shí)現(xiàn),因?yàn)檫@意味著父類也已注冊(cè)了通知屠尊。
如果通知傳遞到類層次結(jié)構(gòu)的頂部旷祸,則
NSObject
拋出,NSInternalInconsistencyException
因?yàn)檫@是編程錯(cuò)誤:子類無(wú)法使用為其注冊(cè)的通知讼昆。
3托享、移除KVO
通過(guò)向被觀察對(duì)象發(fā)送一條removeObserver:forKeyPath:context:
消息,指定observer
浸赫,keyPath
和context
闰围,可以刪除鍵值觀察者。
移除觀察者時(shí)既峡,請(qǐng)謹(jǐn)記以下幾點(diǎn)羡榴。
- 如果移除了一個(gè)沒(méi)有注冊(cè)的觀察者,則將會(huì)引發(fā)一個(gè)
NSRangeException
異常涧狮,你可以將removeObserver:forKeyPath:context:
調(diào)用放在try / catch塊中以處理潛在的異常炕矮。 - 當(dāng)對(duì)象釋放后么夫,觀察者不會(huì)自動(dòng)被移除者冤,如果被觀察對(duì)象也沒(méi)有被釋放,那么被觀察對(duì)象會(huì)繼續(xù)發(fā)送通知档痪,和其他的對(duì)象一樣涉枫,向已釋放的對(duì)象發(fā)送消息,會(huì)觸發(fā)內(nèi)存異常腐螟。為此愿汰,要確保觀察者在對(duì)象釋放之前困后,刪除自己。
- 該協(xié)議無(wú)法詢問(wèn)對(duì)象是觀察者還是被觀察者衬廷。為了代碼不出現(xiàn)相關(guān)的錯(cuò)誤摇予。一種典型的做法是在觀察者初始化期間(例如在中
init
或中viewDidLoad
)注冊(cè)為觀察者,在釋放過(guò)程中(通常在中dealloc
)注銷(確保正確配對(duì)和排序的添加和刪除消息)吗跋,并且在對(duì)象從內(nèi)存中釋放之前將其注銷侧戴。 。
4跌宛、自動(dòng)通知與手動(dòng)通知
KVO
默認(rèn)的是自動(dòng)通知酗宋,也就是當(dāng)我們屬性的值變化的時(shí)候,就會(huì)自動(dòng)發(fā)送通知疆拘,我們可以在改類中重寫automaticallyNotifiesObserversForKey:
方法來(lái)控制是否啟用自動(dòng)通知蜕猫。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return YES;
}
- 返回為
YES
時(shí),是為該對(duì)象的所有屬性啟用自動(dòng)通知哎迄。 - 返回為
NO
時(shí)回右,是為該對(duì)象的所有對(duì)象禁用自動(dòng)通知。
我們可以根據(jù)Key來(lái)判斷漱挚,為某一個(gè)屬性啟用或者禁用自動(dòng)通知楣黍。
另外針對(duì)特定的屬性啟用和禁用自動(dòng)通知,系統(tǒng)還給我們生成了唯一的方法棱烂。
@interface Account : NSObject
@property (nonatomic, assign) double balance;
@property (nonatomic, assign) double interesRate;
@end
以Account
中的屬性為例租漂,編譯器為我們自動(dòng)生成了兩個(gè)方法,分別來(lái)控制該屬性是否啟用自動(dòng)通知颊糜。
+ (BOOL)automaticallyNotifiesObserversOfBalance {
return NO;
}
+ (BOOL)automaticallyNotifiesObserversOfInteresRate {
return NO;
}
automaticallyNotifiesObserversForKey:
方法的優(yōu)先級(jí)大于特定屬性生成的方法哩治,如果實(shí)現(xiàn)了automaticallyNotifiesObserversForKey:
方法,那么特定屬性的方法將不會(huì)被調(diào)用衬鱼。
要實(shí)現(xiàn)手動(dòng)觀察者通知业筏,請(qǐng)手動(dòng)調(diào)用willChangeValueForKey:
在更改值之前和didChangeValueForKey:
更改值之后。以balance
屬性實(shí)現(xiàn)了手動(dòng)通知鸟赫。
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
5蒜胖、可變集合的KVO
當(dāng)我們監(jiān)聽的對(duì)象的屬性是可變集合或者是可變數(shù)組時(shí),如果我們想要得到數(shù)組或者集合內(nèi)容變化時(shí)的通知抛蚤,我們需要做一些特殊的處理台谢。
- 使用
mutableArrayValueForKey:
方法取出對(duì)象中的數(shù)組,然后在對(duì)可變數(shù)組進(jìn)行操作岁经,此時(shí)我們就可以得到數(shù)組內(nèi)容變化的通知了朋沮。
NSMutableArray *mArray = [self.account mutableArrayValueForKey:@"transactions"];
[mArray addObject:@"4"];
- 可變集合的操作和這個(gè)類似,使用
mutableSetValueForKey:
缀壤。
6樊拓、屬性依賴
當(dāng)一個(gè)屬性的值是依賴于其他幾個(gè)屬性來(lái)決定的時(shí)候纠亚,我們可以使用keyPathsForValuesAffectingValueForKey:
方法或者使用遵循命名方式的keyPathsForValuesAffectingValueFor<Key>
來(lái)建立以來(lái)關(guān)系。
例如筋夏,一個(gè)人的全名取決于名字和姓氏蒂胞。返回全名的方法可以編寫如下:
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
我們?cè)谕獠勘O(jiān)聽fullName
,當(dāng)firstName
或者lastName
的值發(fā)生改變時(shí)条篷,則應(yīng)該觸發(fā)回調(diào)啤誊。
下面介紹兩種建立依賴的方法。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
KVO實(shí)現(xiàn)原理
KVO是使用isa-swizzling技術(shù)實(shí)現(xiàn)的拥娄,簡(jiǎn)單來(lái)說(shuō)就是修改了對(duì)象的isa指針蚊锹,使其指向中間類而不是真正的類,所以isa指針的值并不能反映實(shí)例的實(shí)際類稚瘾,所以應(yīng)該使用class
方法來(lái)確定對(duì)象的實(shí)際類牡昆。
1.1、KVO驗(yàn)證
接下來(lái)我們就做一個(gè)簡(jiǎn)單的驗(yàn)證摊欠。
現(xiàn)在我們有一個(gè)Person
類
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
我們分別在添加KVO之前和添加KVO只有來(lái)輸出對(duì)象的isa指針看看丢烘。
self.person = [Person new];
{
Class cls = object_getClass(self.person);
NSLog(@"%@", NSStringFromClass(cls));
}
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
{
Class cls = object_getClass(self.person);
NSLog(@"%@", NSStringFromClass(cls));
}
輸出結(jié)果如下
2020-02-14 10:24:32.252254+0800 KVO原理探索[23368:1376988] Person
2020-02-14 10:24:32.252750+0800 KVO原理探索[23368:1376988] NSKVONotifying_Person
我們發(fā)現(xiàn)兩次輸出的結(jié)果不一樣,對(duì)象沒(méi)有添加KVO之前些椒,isa指針指向的是Person
類播瞳,添加了KVO之后對(duì)象的isa指針,指向的是NSKVONotifying_Person
免糕。至此我們可以得出對(duì)象在添加KVO之后赢乓,在運(yùn)行時(shí)為我們動(dòng)態(tài)的生成了一個(gè)NSKVONotifying_Person
的類,并且將這個(gè)對(duì)象的isa指針指向了這個(gè)新的類石窑。
1.2牌芋、動(dòng)態(tài)類的繼承關(guān)系
我們都知道,在OC中松逊,所有的類躺屁,都有一個(gè)父類,我們來(lái)看看NSKVONotifying_Person
的繼承關(guān)系经宏。
Class cls = object_getClass(self.person);
NSLog(@"%@", NSStringFromClass(cls));
Class supCls = cls;
do {
supCls = [supCls superclass];
NSLog(@"%@", NSStringFromClass(supCls));
} while (supCls);
這段代碼將會(huì)輸出類的所有父類犀暑。
2020-02-14 10:37:24.826558+0800 KVO原理探索[23558:1388700] NSKVONotifying_Person
2020-02-14 10:37:24.826718+0800 KVO原理探索[23558:1388700] Person
2020-02-14 10:37:24.826840+0800 KVO原理探索[23558:1388700] NSObject
2020-02-14 10:37:24.826945+0800 KVO原理探索[23558:1388700] (null)
通過(guò)驗(yàn)證,我們發(fā)現(xiàn)NSKVONotifying_Person
是直接繼承與Person
的烁兰。
1.3耐亏、動(dòng)態(tài)類方法探究
接下來(lái)我們看看這個(gè)動(dòng)態(tài)生成的類中都有那寫方法,我們使用Runtime
的API來(lái)輸出這個(gè)類中的所有方法以及他們的實(shí)現(xiàn)缚柏。
- (void)printClassAllMethod:(Class)cls {
unsigned int count = 0;
Method *methods = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method method = methods[i];
SEL methodSel = method_getName(method);
IMP methodImp = method_getImplementation(method);
NSLog(@"%@-%p", NSStringFromSelector(methodSel), methodImp);
}
free(methods);
}
調(diào)用上面的這段代碼就可以輸出這個(gè)類中都定義了哪些方法苹熏,我們來(lái)看看NSKVONotifying_Person
中都有哪些碟贾。
2020-02-14 11:43:09.706699+0800 KVO原理探索[24582:1444989] setName:-0x7fff25721c7a
2020-02-14 11:43:09.706794+0800 KVO原理探索[24582:1444989] class-0x7fff2572073d
2020-02-14 11:43:09.706871+0800 KVO原理探索[24582:1444989] dealloc-0x7fff257204a2
2020-02-14 11:43:09.706973+0800 KVO原理探索[24582:1444989] _isKVOA-0x7fff2572049a
我們發(fā)現(xiàn)它重寫了三個(gè)方法并且自定義了一個(gè)方法币喧,最主要的是它重寫了屬性的setter
方法轨域。
這里我們也輸出一下Person
類中的所有方法。
2020-02-14 11:41:12.101584+0800 KVO原理探索[24582:1444989] .cxx_destruct-0x108053ee0
2020-02-14 11:41:12.101732+0800 KVO原理探索[24582:1444989] name-0x108053e70
2020-02-14 11:41:12.101854+0800 KVO原理探索[24582:1444989] setName:-0x108053ea0
接下來(lái)我們來(lái)看看杀餐,添加KVO之后設(shè)置屬性時(shí)干发,有哪些變化。
我們可以看到對(duì)象在沒(méi)有添加KVO時(shí)史翘,直接調(diào)用了屬性的setter方法對(duì)屬性進(jìn)行賦值枉长。通過(guò)方法的地址可以驗(yàn)證。
2020-02-14 11:41:12.101854+0800 KVO原理探索[24582:1444989] setName:-0x108053ea0
上面setter方法的地址和調(diào)用地址是一樣的琼讽,由此可以得出是直接調(diào)用了setter方法必峰。
當(dāng)對(duì)象在添加了KVO之后,我們?cè)賹?duì)屬性進(jìn)行賦值的時(shí)候調(diào)用的不一樣了钻蹬。我們發(fā)現(xiàn)這里調(diào)用的方法的地址就是我們動(dòng)態(tài)類中setter方法的地址吼蚁。
2020-02-14 11:43:09.706699+0800 KVO原理探索[24582:1444989] setName:-0x7fff25721c7a
所以當(dāng)對(duì)象添加了KVO之后,再對(duì)屬性進(jìn)行賦值時(shí)調(diào)用的是動(dòng)態(tài)類中重寫的方法问欠。在這個(gè)方法中我們發(fā)現(xiàn)它調(diào)用了willChangeValueForKey:
和didChangeValueForKey:
肝匆,根據(jù)官網(wǎng)的介紹可知,這兩個(gè)方法是用來(lái)發(fā)送通知的顺献。
最后調(diào)用父類的setter方法來(lái)賦值旗国。
2、原理總結(jié)
- 監(jiān)聽者監(jiān)聽
Person
對(duì)象的某一個(gè)屬性的變化注整,系統(tǒng)會(huì)動(dòng)態(tài)為類Person創(chuàng)建一個(gè)子類NSKVONotifying_Person
能曾,并將Person
對(duì)象的isa指針重新指向該子類 - 系統(tǒng)會(huì)重寫
Person
對(duì)象的setter方法。( 賦值前后分別調(diào)用willChangeValueForKey
和didChangeValueForKey
跟蹤新舊值 )肿轨。在對(duì)象賦值時(shí)是調(diào)用父類的setter方法來(lái)處理的借浊。 - 當(dāng)
Person
對(duì)象的屬性發(fā)生改變時(shí),系統(tǒng)通知監(jiān)聽者萝招,調(diào)用observeValueForKey:ofObject:change:context
方法即可蚂斤。
問(wèn)題。
當(dāng)我們的對(duì)象添加了KVO之后槐沼,為什么通過(guò)class
方法獲取到的類是Person
呢曙蒸?
因?yàn)?code>NSKVONotifying_Person重寫了class
方法,在這個(gè)方法中返回為Person
岗钩。但是object_getClass
獲取到的是isa指針纽窟,所以調(diào)用object_getClass
返回的是NSKVONotifying_Person
。