目錄
- 1. 什么是 KVO
- 2. KVO 的基本使用
?2.1 注冊(cè)方法
?2.2 監(jiān)聽(tīng)方法
?2.3 移除方法
?2.4 使用示例
?2.5 實(shí)際應(yīng)用
?2.6 KVO 觸發(fā)監(jiān)聽(tīng)方法的方式
??2.6.1 自動(dòng)觸發(fā)
??2.6.2 手動(dòng)觸發(fā)- 3. KVO 的進(jìn)階使用
?3.1 observationInfo 屬性
?3.2 context 的使用
?3.3 KVO 監(jiān)聽(tīng)集合對(duì)象
?3.4 KVO 的自動(dòng)觸發(fā)控制
?3.5 KVO 的手動(dòng)觸發(fā)
?3.6 KVO 新舊值相等時(shí)不觸發(fā)
?3.7 KVO 手動(dòng)觀察集合屬性
?3.8 KVO 的依賴觀察
??3.8.1 一對(duì)一關(guān)系
??3.8.2 一對(duì)多關(guān)系- 4. KVO 的使用注意
?4.1 移除觀察者的注意點(diǎn)
?4.2 防止多次注冊(cè)和移除相同的 KVO
?4.3 其它注意點(diǎn)- 5. KVO 的實(shí)現(xiàn)原理
?5.1 isa-swizzling
?5.2 KVO 動(dòng)態(tài)生成的子類(lèi)都有哪些方法- 6. FBKVOController
?6.1 系統(tǒng) KVO 的缺點(diǎn)
?6.2 FBKVOController 的介紹
?6.3 FBKVOController 的優(yōu)點(diǎn)
?6.4 FBKVOController 的使用
?6.5 FBKVOController 的解析- 參考
1. 什么是 KVO
-
KVO
的全稱是Key-Value Observing
胰苏,俗稱“鍵值觀察/監(jiān)聽(tīng)”蝶缀,是蘋(píng)果提供的一套事件通知機(jī)制袁波,允許一個(gè)對(duì)象觀察/監(jiān)聽(tīng)另一個(gè)對(duì)象指定屬性值的改變。當(dāng)被觀察對(duì)象屬性值發(fā)生改變時(shí)球及,會(huì)觸發(fā)KVO
的監(jiān)聽(tīng)方法來(lái)通知觀察者。KVO
是在MVC
應(yīng)用程序中的各層之間進(jìn)行通信的一種特別有用的技術(shù)台舱。 -
KVO
和NSNotification
都是iOS
中觀察者模式的一種實(shí)現(xiàn)尚骄。 -
KVO
可以監(jiān)聽(tīng)單個(gè)屬性的變化,也可以監(jiān)聽(tīng)集合對(duì)象的變化毙死。監(jiān)聽(tīng)集合對(duì)象變化時(shí)燎潮,需要通過(guò)KVC
的mutableArrayValueForKey:
等可變代理方法獲得集合代理對(duì)象,并使用代理對(duì)象進(jìn)行操作扼倘,當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí)确封,會(huì)觸發(fā)KVO
的監(jiān)聽(tīng)方法。集合對(duì)象包含NSArray
和NSSet
再菊。 -
KVO
和KVC
有著密切的關(guān)系爪喘,如果想要深入了解KVO
,建議先學(xué)習(xí)KVC
袄简。
傳送門(mén):iOS - 關(guān)于 KVC 的一些總結(jié)
2. KVO 的基本使用
KVO
使用三部曲:添加/注冊(cè)KVO
監(jiān)聽(tīng)、實(shí)現(xiàn)監(jiān)聽(tīng)方法以接收屬性改變通知泛啸、 移除KVO
監(jiān)聽(tīng)绿语。
- 調(diào)用方法
addObserver:forKeyPath:options:context:
給被觀察對(duì)象添加觀察者; - 在觀察者類(lèi)中實(shí)現(xiàn)
observeValueForKeyPath:ofObject:change:context:
方法以接收屬性改變的通知消息; - 當(dāng)觀察者不需要再監(jiān)聽(tīng)時(shí)吕粹,調(diào)用
removeObserver:forKeyPath:
方法將觀察者移除种柑。需要注意的是,至少需要在觀察者銷(xiāo)毀之前匹耕,調(diào)用此方法聚请,否則可能會(huì)導(dǎo)致Crash
。
2.1 注冊(cè)方法
/*
** target: 被觀察對(duì)象
** observer:觀察者對(duì)象
** keyPath: 被觀察對(duì)象的屬性的關(guān)鍵路徑稳其,不能為nil
** options: 觀察的配置選項(xiàng)驶赏,包括觀察的內(nèi)容(枚舉類(lèi)型):
NSKeyValueObservingOptionNew:觀察新值
NSKeyValueObservingOptionOld:觀察舊值
NSKeyValueObservingOptionInitial:觀察初始值,如果想在注冊(cè)觀察者后既鞠,立即接收一次回調(diào)煤傍,可以加入該枚舉值
NSKeyValueObservingOptionPrior:分別在值改變前后觸發(fā)方法(即一次修改有兩次觸發(fā))
** context: 可以傳入任意數(shù)據(jù)(任意類(lèi)型的對(duì)象或者C指針),在監(jiān)聽(tīng)方法中可以接收到這個(gè)數(shù)據(jù)嘱蛋,是KVO中的一種傳值方式
如果傳的是一個(gè)對(duì)象蚯姆,必須在移除觀察之前持有它的強(qiáng)引用,否則在監(jiān)聽(tīng)方法中訪問(wèn)context就可能導(dǎo)致Crash
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
2.2 監(jiān)聽(tīng)方法
如果對(duì)象被注冊(cè)成為觀察者洒敏,則該對(duì)象必須能響應(yīng)以下監(jiān)聽(tīng)方法龄恋,即該對(duì)象所屬類(lèi)中必須實(shí)現(xiàn)監(jiān)聽(tīng)方法。當(dāng)被觀察對(duì)象屬性發(fā)生改變時(shí)就會(huì)調(diào)用監(jiān)聽(tīng)方法凶伙。如果沒(méi)有實(shí)現(xiàn)就會(huì)導(dǎo)致Crash
郭毕。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/*
** keyPath:被觀察對(duì)象的屬性的關(guān)鍵路徑
** object: 被觀察對(duì)象
** change: 字典 NSDictionary<NSKeyValueChangeKey, id>,屬性值更改的詳細(xì)信息镊靴,根據(jù)注冊(cè)方法中options參數(shù)傳入的枚舉來(lái)返回
key為 NSKeyValueChangeKey 枚舉類(lèi)型
{
1.NSKeyValueChangeKindKey:存儲(chǔ)本次改變的信息(change字典中默認(rèn)包含這個(gè)key)
{
對(duì)應(yīng)枚舉類(lèi)型 NSKeyValueChange
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
如果是對(duì)被觀察對(duì)象屬性(包括集合)進(jìn)行賦值操作铣卡,kind 字段的值為 NSKeyValueChangeSetting
如果被觀察的是集合對(duì)象,且進(jìn)行的是(插入偏竟、刪除煮落、替換)操作,則會(huì)根據(jù)集合對(duì)象的操作方式來(lái)設(shè)置 kind 字段的值
插入:NSKeyValueChangeInsertion
刪除:NSKeyValueChangeRemoval
替換:NSKeyValueChangeReplacement
}
2.NSKeyValueChangeNewKey:存儲(chǔ)新值(如果options中傳入NSKeyValueObservingOptionNew踊谋,change字典中就會(huì)包含這個(gè)key)
3.NSKeyValueChangeOldKey:存儲(chǔ)舊值(如果options中傳入NSKeyValueObservingOptionOld蝉仇,change字典中就會(huì)包含這個(gè)key)
4.NSKeyValueChangeIndexesKey:如果被觀察的是集合對(duì)象,且進(jìn)行的是(插入殖蚕、刪除轿衔、替換)操作,則change字典中就會(huì)包含這個(gè)key
這個(gè)key的value是一個(gè)NSIndexSet對(duì)象睦疫,包含更改關(guān)系中的索引
5.NSKeyValueChangeNotificationIsPriorKey:如果options中傳入NSKeyValueObservingOptionPrior害驹,則在改變前通知的change字典中會(huì)包含這個(gè)key。
這個(gè)key對(duì)應(yīng)的value是NSNumber包裝的YES蛤育,我們可以這樣來(lái)判斷是不是在改變前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES]
}
** context:注冊(cè)方法中傳入的context
*/
}
2.3 移除方法
在調(diào)用注冊(cè)方法后宛官,KVO
并不會(huì)對(duì)觀察者進(jìn)行強(qiáng)引用葫松,所以需要注意觀察者的生命周期。至少需要在觀察者銷(xiāo)毀之前底洗,調(diào)用以下方法移除觀察者腋么,否則如果在觀察者被釋放后,再次觸發(fā)KVO
監(jiān)聽(tīng)方法就會(huì)導(dǎo)致Crash
亥揖。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
2.4 使用示例
以下使用KVO
為person
對(duì)象添加觀察者為當(dāng)前viewController
珊擂,監(jiān)聽(tīng)person
對(duì)象的name
屬性值的改變。當(dāng)name
值改變時(shí)费变,觸發(fā)KVO
的監(jiān)聽(tīng)方法摧扇。
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person.name= @"張三";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"keyPath:%@",keyPath);
NSLog(@"object:%@",object);
NSLog(@"change:%@",change);
NSLog(@"context:%@",context);
}
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"name"];
}
keyPath:name
object:<HTPerson: 0x600003ae4340>
change:{ kind = 1; new = "\U70b9\U51fb"; old = "<null>"; }
context:(null)
2.5 實(shí)際應(yīng)用
KVO
主要用來(lái)做鍵值觀察操作,想要一個(gè)值發(fā)生改變后通知另一個(gè)對(duì)象胡控,則用KVO
實(shí)現(xiàn)最為合適扳剿。斯坦福大學(xué)的iOS
教程中有一個(gè)很經(jīng)典的案例,通過(guò)KVO
在Model
和Controller
之間進(jìn)行通信昼激。如圖所示:
2.6 KVO觸發(fā)監(jiān)聽(tīng)方法的方式
KVO
觸發(fā)分為自動(dòng)觸發(fā)和手動(dòng)觸發(fā)兩種方式庇绽。
2.6.1 自動(dòng)觸發(fā)
① 如果是監(jiān)聽(tīng)對(duì)象特定屬性值的改變,通過(guò)以下方式改變屬性值會(huì)觸發(fā)KVO
:
- 使用點(diǎn)語(yǔ)法
- 使用
setter
方法 - 使用
KVC
的setValue:forKey:
方法 - 使用
KVC
的setValue:forKeyPath:
方法
② 如果是監(jiān)聽(tīng)集合對(duì)象的改變橙困,需要通過(guò)KVC
的mutableArrayValueForKey:
等方法獲得代理對(duì)象瞧掺,并使用代理對(duì)象進(jìn)行操作,當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí)凡傅,會(huì)觸發(fā)KVO
辟狈。集合對(duì)象包含NSArray
和NSSet
。
2.6.2 手動(dòng)觸發(fā)
① 普通對(duì)象屬性或是成員變量使用:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
② NSArray
對(duì)象使用:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
③ NSSet
對(duì)象使用:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
3. KVO 的進(jìn)階使用
3.1 observationInfo 屬性
-
observationInfo
屬性是NSKeyValueObserving.h
文件中系統(tǒng)通過(guò)分類(lèi)給NSObject
添加的屬性夏跷,所以所有繼承于NSObject
的對(duì)象都含有該屬性哼转; - 可以通過(guò)
observationInfo
屬性查看被觀察對(duì)象的全部觀察信息,包括observer
槽华、keyPath
壹蔓、options
、context
等猫态。
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
3.2 context 的使用
注冊(cè)方法addObserver:forKeyPath:options:context:
中的context
可以傳入任意數(shù)據(jù)佣蓉,并且可以在監(jiān)聽(tīng)方法中接收到這個(gè)數(shù)據(jù)。
context
作用:標(biāo)簽-區(qū)分亲雪,可以更精確的確定被觀察對(duì)象屬性勇凭,用于繼承、 多監(jiān)聽(tīng)义辕;也可以用來(lái)傳值虾标。
??KVO
只有一個(gè)監(jiān)聽(tīng)回調(diào)方法observeValueForKeyPath:ofObject:change:context:
,我們通常情況下可以在注冊(cè)方法中指定context
為NULL
灌砖,并在監(jiān)聽(tīng)方法中通過(guò)object
和keyPath
來(lái)判斷觸發(fā)KVO
的來(lái)源璧函。
??但是如果存在繼承的情況贞让,比如現(xiàn)在有 Person 類(lèi)和它的兩個(gè)子類(lèi) Teacher 類(lèi)和 Student 類(lèi),person柳譬、teacher 和 student 實(shí)例對(duì)象都對(duì) account 對(duì)象的 balance 屬性進(jìn)行觀察。問(wèn)題:
??① 當(dāng) balance 發(fā)生改變時(shí)续镇,應(yīng)該由誰(shuí)來(lái)處理呢美澳?
??② 如果都由 person 來(lái)處理,那么在 Person 類(lèi)的監(jiān)聽(tīng)方法中又該怎么判斷是自己的事務(wù)還是子類(lèi)對(duì)象的事務(wù)呢摸航?
??這時(shí)候通過(guò)使用context
就可以很好地解決這個(gè)問(wèn)題制跟,在注冊(cè)方法中為context
設(shè)置一個(gè)獨(dú)一無(wú)二的值,然后在監(jiān)聽(tīng)方法中對(duì)context
值進(jìn)行檢驗(yàn)即可酱虎。蘋(píng)果的推薦用法:用
context
來(lái)精確的確定被觀察對(duì)象屬性雨膨,使用唯一命名的靜態(tài)變量的地址作為context
的值《链可以為整個(gè)類(lèi)設(shè)置一個(gè)context
聊记,然后在監(jiān)聽(tīng)方法中通過(guò)object
和keyPath
來(lái)確定被觀察屬性,這樣存在繼承的情況就可以通過(guò)context
來(lái)判斷恢暖;也可以為每個(gè)被觀察對(duì)象屬性設(shè)置不同的context
排监,這樣使用context
就可以精確的確定被觀察對(duì)象屬性。
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];
}
- (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];
}
}
-
context
優(yōu)點(diǎn):嵌套少杰捂、性能高舆床、更安全、擴(kuò)展性強(qiáng)嫁佳。 -
context
注意點(diǎn):
① 如果傳的是一個(gè)對(duì)象挨队,必須在移除觀察之前持有它的強(qiáng)引用,否則在監(jiān)聽(tīng)方法中訪問(wèn)context
就可能導(dǎo)致Crash
蒿往;
② 空傳NULL
而不應(yīng)該傳nil
盛垦。
3.3 KVO 監(jiān)聽(tīng)集合對(duì)象
KVO
可以監(jiān)聽(tīng)單個(gè)屬性的變化,也可以監(jiān)聽(tīng)集合對(duì)象的變化熄浓。監(jiān)聽(tīng)集合對(duì)象變化時(shí)情臭,需要通過(guò)KVC
的mutableArrayValueForKey:
等方法獲得代理對(duì)象,并使用代理對(duì)象進(jìn)行操作赌蔑,當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí)俯在,會(huì)觸發(fā)KVO
的監(jiān)聽(tīng)方法。集合對(duì)象包含NSArray
和NSSet
娃惯。
(注意:如果直接對(duì)集合對(duì)象進(jìn)行操作改變跷乐,不會(huì)觸發(fā)KVO
。)
示例代碼及輸出如下:
觀察者 viewController 對(duì)被觀察對(duì)象 person 的 mArray 屬性進(jìn)行監(jiān)聽(tīng)趾浅。
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
self.person.mArray = [NSMutableArray arrayWithCapacity:5];
[self.person addObserver:self forKeyPath:@"mArray" options:(NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self.person.mArray addObject:@"2"]; //如果直接對(duì)數(shù)組進(jìn)行操作愕提,不會(huì)觸發(fā)KVO
NSMutableArray *array = [self.person mutableArrayValueForKey:@"mArray"];
[array addObject:@"1"];
[array replaceObjectAtIndex:0 withObject:@"2"];
[array removeObjectAtIndex:0];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/* change 字典的值為:
{
indexes:對(duì)應(yīng)的值為數(shù)組操作的詳細(xì)信息馒稍,包括索引等
kind: 對(duì)應(yīng)的值為數(shù)組操作的方式:
2:代表插入操作
3:代表刪除操作
4:代表替換操作
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
new/old:如果是插入操作,則字典中只會(huì)有new字段浅侨,對(duì)應(yīng)的值為插入的元素纽谒,前提條件是options中傳入了(NSKeyValueObservingOptionNew)
如果是刪除操作,則字典中只會(huì)有old字段如输,對(duì)應(yīng)的值為刪除的元素鼓黔,前提條件是options中傳入了(NSKeyValueObservingOptionOld)
如果是替換操作,則字典中new和old字段都可以存在不见,對(duì)應(yīng)的值為替換后的元素和替換前的元素澳化,前提條件是options中傳入了(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
如: indexes = "<_NSCachedIndexSet: 0x600001d092e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
1
);
}
*/
NSLog(@"%@",change);
}
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"mArray"];
}
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 4; new = (2); old = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 3; old = (2); }
3.4 KVO 的自動(dòng)觸發(fā)控制
??可以在被觀察對(duì)象的類(lèi)中重寫(xiě)+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
方法來(lái)控制KVO
的自動(dòng)觸發(fā)。
??如果我們只允許外界觀察 person 的 name 屬性稳吮,可以在 Person 類(lèi)如下操作缎谷。這樣外界就只能觀察 name 屬性,即使外界注冊(cè)了對(duì) person 對(duì)象其它屬性的監(jiān)聽(tīng)灶似,那么在屬性發(fā)生改變時(shí)也不會(huì)觸發(fā)KVO
列林。
// 返回值代表允不允許觸發(fā) KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"name"]) {
automatic = YES;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
??也可以實(shí)現(xiàn)遵循命名規(guī)則為+ (BOOL)automaticallyNotifiesObserversOf<Key>
的方法來(lái)單一控制屬性的KVO
自動(dòng)觸發(fā),<Key>
為屬性名(首字母大寫(xiě))酪惭。
+ (BOOL)automaticallyNotifiesObserversOfName
{
return NO;
}
注意:
- 第一個(gè)方法的優(yōu)先級(jí)高于第二個(gè)方法席纽。如果實(shí)現(xiàn)了
automaticallyNotifiesObserversForKey:
方法,并對(duì)<Key>
做了處理撞蚕,則系統(tǒng)就不會(huì)再調(diào)用該<Key>
的automaticallyNotifiesObserversOf<Key>
方法润梯。options
指定的NSKeyValueObservingOptionInitial
觸發(fā)的KVO
通知,是無(wú)法被automaticallyNotifiesObserversForKey:
阻止的甥厦。
3.5 KVO 的手動(dòng)觸發(fā)
使用場(chǎng)景:
- 使用
KVO
監(jiān)聽(tīng)成員變量值的改變纺铭; - 在某些需要控制監(jiān)聽(tīng)過(guò)程的場(chǎng)景下。比如:為了盡量減少不必要的觸發(fā)通知操作刀疙,或者當(dāng)多個(gè)更改同時(shí)具備的時(shí)候才調(diào)用屬性改變的監(jiān)聽(tīng)方法舶赔。
??由于KVO
的本質(zhì),重寫(xiě)setter
方法來(lái)達(dá)到可以通知所有觀察者對(duì)象的目的谦秧,所以只有通過(guò)setter
方法或KVC
方法去修改屬性變量值的時(shí)候竟纳,才會(huì)觸發(fā)KVO
,直接修改成員變量不會(huì)觸發(fā)KVO
疚鲤。
??當(dāng)我們要使用KVO
監(jiān)聽(tīng)成員變量值改變的時(shí)候锥累,可以通過(guò)在為成員變量賦值的前后手動(dòng)調(diào)用willChangeValueForKey:
和didChangeValueForKey:
兩個(gè)方法來(lái)手動(dòng)觸發(fā)KVO
,如:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person willChangeValueForKey:@"age"];
self.person->_age = 18;
[self.person didChangeValueForKey:@"age"];
}
??NSKeyValueObservingOptionPrior
(分別在值改變前后觸發(fā)方法集歇,即一次修改有兩次觸發(fā))的兩次觸發(fā)分別在willChangeValueForKey:
和didChangeValueForKey:
的時(shí)候進(jìn)行的桶略。
??如果注冊(cè)方法中options
傳入NSKeyValueObservingOptionPrior
,那么可以通過(guò)只調(diào)用willChangeValueForKey:
來(lái)觸發(fā)改變前的那次KVO
,可以用于在屬性值即將更改前做一些操作际歼。
3.6 KVO 新舊值相等時(shí)不觸發(fā)
??有時(shí)候我們可能會(huì)有這樣的需求惶翻,KVO
監(jiān)聽(tīng)的屬性值修改前后相等的時(shí)候,不觸發(fā)KVO
的監(jiān)聽(tīng)方法鹅心,可以結(jié)合KVO
的自動(dòng)觸發(fā)控制和手動(dòng)觸發(fā)來(lái)實(shí)現(xiàn)吕粗。
??例如:對(duì) person 對(duì)象的 name 屬性注冊(cè)了KVO
監(jiān)聽(tīng),我們希望在對(duì) name 屬性賦值時(shí)做一個(gè)判斷旭愧,如果新值和舊值相等溯泣,則不觸發(fā)KVO
,可以在 Person 類(lèi)中如下這樣實(shí)現(xiàn)榕茧,將 name 屬性值改變的KVO
觸發(fā)方式由自動(dòng)觸發(fā)改為手動(dòng)觸發(fā)。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = YES;
if ([key isEqualToString:@"name"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)setName:(NSString *)name
{
if (![_name isEqualToString:name]) {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
3.7 KVO 手動(dòng)觀察集合屬性
有些情況下我們想手動(dòng)觀察集合屬性客给,下面以觀察數(shù)組為例用押。
關(guān)鍵方法:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
需要注意的是,根據(jù)KVC
的NSMutableArray 搜索模式
:
傳送門(mén):iOS - 關(guān)于 KVC 的一些總結(jié)
- 至少要實(shí)現(xiàn)一個(gè)插入和一個(gè)刪除方法靶剑,否則不會(huì)觸發(fā)
KVO
蜻拨。如
插入方法:insertObject:in<Key>AtIndex:
或insert<Key>:atIndexes:
刪除方法:removeObjectFrom<Key>AtIndex:
或remove<Key>AtIndexes:
- 可以不實(shí)現(xiàn)替換方法,但是如果不實(shí)現(xiàn)替換方法桩引,執(zhí)行替換操作時(shí)缎讼,
KVO
會(huì)把它當(dāng)成先刪除后添加,即會(huì)觸發(fā)兩次KVO
坑匠。第一次觸發(fā)的KVO
中change
字典的old
鍵的值為替換前的元素血崭,第二次觸發(fā)的KVO
中change
字典的new
鍵的值為替換后的元素,前提條件是注冊(cè)方法中的options
傳入對(duì)應(yīng)的枚舉值厘灼。 - 如果實(shí)現(xiàn)替換方法夹纫,則執(zhí)行替換操作只會(huì)觸發(fā)一次
KVO
,并且change
字典會(huì)同時(shí)包含new
和old
设凹,前提條件是注冊(cè)方法中的options
傳入對(duì)應(yīng)的枚舉值舰讹。
替換方法:replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
- 建議實(shí)現(xiàn)替換方法以提高性能。
示例代碼如下:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"mArray"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)insertMArray:(NSArray *)array atIndexes:(NSIndexSet *)indexes
{
[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray insertObjects:array atIndexes:indexes];
[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
}
- (void)removeMArrayAtIndexes:(NSIndexSet *)indexes
{
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray removeObjectsAtIndexes:indexes];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
}
- (void)replaceMArrayAtIndexes:(NSIndexSet *)indexes withMArray:(NSArray *)array
{
[self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray replaceObjectsAtIndexes:indexes withObjects:array];
[self didChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
}
3.8 KVO 的依賴觀察
3.8.1 一對(duì)一關(guān)系
??有些情況下闪朱,一個(gè)屬性的改變依賴于別的一個(gè)或多個(gè)屬性的改變月匣,也就是說(shuō)當(dāng)別的屬性改了,這個(gè)屬性也會(huì)跟著改變奋姿。
??比如我們想要對(duì) Download 類(lèi)中的 downloadProgress 屬性進(jìn)行KVO
監(jiān)聽(tīng)锄开,該屬性的改變依賴于 writtenData 和 totalData 屬性的改變。觀察者監(jiān)聽(tīng)了 downloadProgress 称诗,當(dāng) writtenData 和 totalData 屬性值改變時(shí)院刁,觀察者也應(yīng)該被通知。以下有兩種方法可以解決這個(gè)問(wèn)題粪狼。
- 重寫(xiě)以下方法來(lái)指明 downloadProgress 屬性依賴于 writtenData 和 totalData:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"writtenData",@"totalData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- 實(shí)現(xiàn)一個(gè)遵循命名規(guī)則為
keyPathsForValuesAffecting<Key>
的類(lèi)方法退腥,<Key>
是依賴于其他值的屬性名(首字母大寫(xiě)):
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress
{
return [NSSet setWithObjects:@"writtenData",@"totalData", nil];
}
注意:以上兩個(gè)方法可以同時(shí)存在任岸,且都會(huì)調(diào)用,但是最終結(jié)果會(huì)以
keyPathsForValuesAffectingValueForKey:
為準(zhǔn)狡刘。
3.8.2 一對(duì)多關(guān)系
??以上方法在觀察集合屬性時(shí)就不管用了享潜。例如,假如你有一個(gè) Department 類(lèi)嗅蔬,它有一個(gè)裝有 Employee 類(lèi)的實(shí)例對(duì)象的數(shù)組剑按,Employee 類(lèi)有 salary 屬性。你希望 Department 類(lèi)有一個(gè) totalSalary 屬性來(lái)計(jì)算所有員工的薪水澜术,也就是在這個(gè)關(guān)系中 Department 的 totalSalary 依賴于所有 Employee 實(shí)例對(duì)象的 salary 屬性艺蝴。以下有兩種方法可以解決這個(gè)問(wèn)題。
- 你可以用
KVO
將 parent(比如 Department )作為所有 children(比如 Employee )相關(guān)屬性的觀察者鸟废。你必須在把 child 添加或刪除到 parent 時(shí)把 parent 作為 child 的觀察者添加或刪除猜敢。在observeValueForKeyPath:ofObject:change:context:
方法中我們可以針對(duì)被依賴項(xiàng)的變更來(lái)更新依賴項(xiàng)的值:
#import "Department.h"
static void *totalSalaryContext = &totalSalaryContext;
@interface Department ()
@property (nonatomic,strong)NSArray<Employee *> *employees;
@property (nonatomic,strong)NSNumber *totalSalary;
@end
@implementation Department
- (instancetype)initWithEmployees:(NSArray *)employees
{
self = [super init];
if (self) {
self.employees = [employees copy];
for (Employee *em in self.employees) {
[em addObserver:self forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:totalSalaryContext];
}
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)setTotalSalary:(NSNumber *)totalSalary
{
if (_totalSalary != totalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = totalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (void)dealloc
{
for (Employee *em in self.employees) {
[em removeObserver:self forKeyPath:@"salary" context:totalSalaryContext];
}
}
@end
- 使用
iOS
中觀察者模式的另一種實(shí)現(xiàn)方式:通知 (NSNotification
) 。
4. KVO的使用注意
4.1 移除觀察者的注意點(diǎn)
- 在調(diào)用
KVO
注冊(cè)方法后盒延,KVO
并不會(huì)對(duì)觀察者進(jìn)行強(qiáng)引用缩擂,所以需要注意觀察者的生命周期。至少需要在觀察者銷(xiāo)毀之前添寺,調(diào)用KVO
移除方法移除觀察者胯盯,否則如果在觀察者被釋放后,再次觸發(fā)KVO
監(jiān)聽(tīng)方法就會(huì)導(dǎo)致Crash
计露。 -
KVO
的注冊(cè)方法和移除方法應(yīng)該是成對(duì)的博脑,如果重復(fù)調(diào)用移除方法,就會(huì)拋出異常NSRangeException
并導(dǎo)致程序Crash
票罐。 - 蘋(píng)果官方推薦的方式是趋厉,在觀察者初始化期間(
init
或者viewDidLoad
的時(shí)候)注冊(cè)為觀察者,在釋放過(guò)程中(dealloc
時(shí))調(diào)用移除方法胶坠,這樣可以保證它們是成對(duì)出現(xiàn)的君账,是一種比較理想的使用方式。
4.2 防止多次注冊(cè)和移除相同的KVO
??有時(shí)候我們難以避免多次注冊(cè)和移除相同的KVO
沈善,或者移除了一個(gè)未注冊(cè)的觀察者乡数,從而產(chǎn)生可能會(huì)導(dǎo)致Crash
的風(fēng)險(xiǎn)。
??三種解決方案:黑科技防止多次添加刪除KVO出現(xiàn)的問(wèn)題
- 利用
@try @catch
(只能針對(duì)刪除多次KVO
的情況下)
給NSObject
增加一個(gè)分類(lèi)闻牡,然后利用Runtime API
交換系統(tǒng)的removeObserver
方法净赴,在里面添加@try @catch
; - 利用 模型數(shù)組 進(jìn)行存儲(chǔ)記錄罩润;
- 利用
observationInfo
里私有屬性玖翅。
4.3 其它注意點(diǎn)
- 如果對(duì)象被注冊(cè)成為觀察者,則該對(duì)象必須能響應(yīng)監(jiān)聽(tīng)方法,即該對(duì)象所屬類(lèi)中必須實(shí)現(xiàn)監(jiān)聽(tīng)方法金度。當(dāng)被觀察對(duì)象屬性發(fā)生改變時(shí)就會(huì)調(diào)用監(jiān)聽(tīng)方法应媚。如果沒(méi)有實(shí)現(xiàn)就會(huì)導(dǎo)致
Crash
。所以KVO
三部曲缺一不可猜极。 -
keyPath
傳入的是一個(gè)字符串乾忱,為避免寫(xiě)錯(cuò)址貌,可以使用NSStringFromSelector(@selector(propertyName))
贸诚,將屬性的getter
方法SEL
轉(zhuǎn)換成字符串秧了,在編譯階段對(duì)keyPath
進(jìn)行檢驗(yàn)。 - 如果注冊(cè)方法中
context
傳的是一個(gè)對(duì)象受扳,必須在移除觀察之前持有它的強(qiáng)引用携龟,否則在監(jiān)聽(tīng)方法中訪問(wèn)context
就可能導(dǎo)致Crash
。- 可以使用
__bridge_retained
橋接剝奪對(duì)象的內(nèi)存管理權(quán)勘高,但必須記得在不需要該對(duì)象時(shí)釋放它峡蟋,否則內(nèi)存泄露。關(guān)于橋接可以參閱《iOS - 老生常談內(nèi)存管理(三):ARC 面世 —— Toll-Free Bridging》相满; - 或者使用全局變量。
- 可以使用
- 如果是監(jiān)聽(tīng)集合對(duì)象的改變桦卒,需要通過(guò)
KVC
的mutableArrayValueForKey:
等方法獲得代理對(duì)象立美,并使用代理對(duì)象進(jìn)行操作,當(dāng)代理對(duì)象的內(nèi)部對(duì)象發(fā)生改變時(shí)方灾,會(huì)觸發(fā)KVO
建蹄。如果直接對(duì)集合對(duì)象進(jìn)行操作改變,不會(huì)觸發(fā)KVO
裕偿。 - 在觀察者類(lèi)的監(jiān)聽(tīng)方法中洞慎,應(yīng)該為無(wú)法識(shí)別的
context
或者object
、keyPath
調(diào)用父類(lèi)的實(shí)現(xiàn)[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
嘿棘。
5. KVO的實(shí)現(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 theclass
method to determine the class of an object instance.
??以上是蘋(píng)果官方對(duì)KVO
實(shí)現(xiàn)的解釋?zhuān)徽f(shuō)明了KVO
是使用isa-swizzling
技術(shù)來(lái)實(shí)現(xiàn)的劲腿,并沒(méi)有做過(guò)多介紹。
5.1 isa-swizzling
??蘋(píng)果使用了isa
混寫(xiě)技術(shù)(isa-swizzling
)來(lái)實(shí)現(xiàn)KVO
鸟妙。當(dāng)我們調(diào)用了addObserver:forKeyPath:options:context:
方法焦人,為instance
被觀察對(duì)象添加KVO
監(jiān)聽(tīng)后,系統(tǒng)會(huì)在運(yùn)行時(shí)利用Runtime API
動(dòng)態(tài)創(chuàng)建instance
對(duì)象所屬類(lèi)A
的子類(lèi)NSKVONotifying_A
重父,并且讓instance
對(duì)象的isa
指向這個(gè)全新的子類(lèi)花椭,并重寫(xiě)原類(lèi)A
的被觀察屬性的setter
方法來(lái)達(dá)到可以通知所有觀察者對(duì)象的目的。
??這個(gè)子類(lèi)的isa
指針指向它自己的meta-class
對(duì)象房午,而不是原類(lèi)的meta-class
對(duì)象矿辽。
??重寫(xiě)的setter
方法的SEL
對(duì)應(yīng)的IMP
為Foundation
中的_NSSetXXXValueAndNotify
函數(shù)(XXX
為Key
的數(shù)據(jù)類(lèi)型),當(dāng)被觀察對(duì)象的屬性發(fā)送改變時(shí),會(huì)調(diào)用_NSSetXXXValueAndNotify
函數(shù)袋倔,這個(gè)函數(shù)中會(huì)調(diào)用:
-
willChangeValueForKey:
方法 - 父類(lèi)原來(lái)的
setter
方法 -
didChangeValueForKey:
方法(內(nèi)部會(huì)觸發(fā)監(jiān)聽(tīng)器即觀察對(duì)象observer
的監(jiān)聽(tīng)方法:observeValueForKeyPath:ofObject:change:context:
)
??在移除KVO
監(jiān)聽(tīng)后雕蔽,被觀察對(duì)象的isa
會(huì)指回原類(lèi)A
,但是NSKVONotifying_A
類(lèi)并沒(méi)有銷(xiāo)毀奕污,還保存在內(nèi)存中萎羔。
5.2 KVO 動(dòng)態(tài)生成的子類(lèi)都有哪些方法
??NSKVONotifying_A
除了重寫(xiě)了setter
方法,還重寫(xiě)了class
碳默、dealloc
贾陷、_isKVOA
這三個(gè)方法(可以使用runtime
的class_copyMethodList
函數(shù)打印方法列表獲得),其中:
-
class
:class
方法中返回的是父類(lèi)的class
對(duì)象嘱根,目的是為了不讓外界知道KVO
動(dòng)態(tài)生成類(lèi)的存在髓废; -
dealloc
:釋放KVO
使用過(guò)程中產(chǎn)生的東西; -
_isKVOA
:用來(lái)標(biāo)志它是一個(gè)KVO
的類(lèi)该抒。
6. FBKVOController
6.1 系統(tǒng) KVO 的缺點(diǎn)
- 使用比較麻煩慌洪,需要三個(gè)步驟:添加/注冊(cè)
KVO
監(jiān)聽(tīng)、實(shí)現(xiàn)監(jiān)聽(tīng)方法以接收屬性改變通知凑保、 移除KVO
監(jiān)聽(tīng)冈爹,缺一不可; - 需要手動(dòng)移除觀察者欧引,移除觀察者的時(shí)機(jī)必須合適频伤,還不能重復(fù)移除;
- 注冊(cè)觀察者的代碼和事件發(fā)生處的代碼上下文不同芝此,傳遞上下文
context
是通過(guò)void *
指針憋肖; - 需要實(shí)現(xiàn)
-observeValueForKeyPath:ofObject:change:context:
方法,比較麻煩婚苹; - 在復(fù)雜的業(yè)務(wù)邏輯中岸更,準(zhǔn)確判斷被觀察者相對(duì)比較麻煩,有多個(gè)被觀測(cè)的對(duì)象和屬性時(shí)膊升,需要在方法中寫(xiě)大量的
if
進(jìn)行判斷怎炊。
6.2 FBKVOController 的介紹
FBKVOController
是 Facebook 開(kāi)源的一個(gè)基于系統(tǒng)KVO
實(shí)現(xiàn)的框架。支持Objective-C
和Swift
語(yǔ)言廓译。
GitHub:https://github.com/facebook/KVOController
6.3 FBKVOController 的優(yōu)點(diǎn)
- 會(huì)自動(dòng)移除觀察者结胀;
- 函數(shù)式編程,可以一行代碼實(shí)現(xiàn)系統(tǒng)
KVO
的三個(gè)步驟责循; - 實(shí)現(xiàn)
KVO
與事件發(fā)生處的代碼上下文相同糟港,不需要跨方法傳參數(shù); - 增加了
block
和SEL
自定義操作對(duì)NSKeyValueObserving
回調(diào)的處理支持院仿; - 每一個(gè)
keyPath
會(huì)對(duì)應(yīng)一個(gè)block
或者SEL
秸抚,不需要使用if
判斷keyPath
速和; - 可以同時(shí)對(duì)一個(gè)對(duì)象的多個(gè)屬性進(jìn)行監(jiān)聽(tīng),寫(xiě)法簡(jiǎn)潔剥汤;
- 線程安全颠放。
6.4 FBKVOController 的使用
FBKVOController
實(shí)現(xiàn)了觀察者和被觀察者的角色反轉(zhuǎn),系統(tǒng)的KVO
是被觀察者添加觀察者吭敢,而FBKVO
實(shí)現(xiàn)了觀察者主動(dòng)去添加被觀察者碰凶,實(shí)現(xiàn)了角色上的反轉(zhuǎn),使用比較方便鹿驼。
// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;
// observe clock date property
// 使用 block
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
// update clock view with new value
clockView.date = change[NSKeyValueChangeNewKey];
}];
// 使用 SEL
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];
6.5 FBKVOController 的解析
如何優(yōu)雅地使用KVO(簡(jiǎn)書(shū))
iOS - FBKVOController 實(shí)現(xiàn)原理(簡(jiǎn)書(shū))
參考
Key-Value Observing Programming Guide(蘋(píng)果官方文檔)
iOS - 關(guān)于 KVC 的一些總結(jié)(簡(jiǎn)書(shū))
KVO原理分析及使用進(jìn)階(簡(jiǎn)書(shū))
iOS開(kāi)發(fā) - 黑科技防止多次添加刪除KVO出現(xiàn)的問(wèn)題(簡(jiǎn)書(shū))
談?wù)?KVO(簡(jiǎn)書(shū))
GitHub/facebook/KVOController(GitHub)
如何優(yōu)雅地使用KVO(簡(jiǎn)書(shū))
iOS - FBKVOController 實(shí)現(xiàn)原理(簡(jiǎn)書(shū))