目錄
- 1. 什么是 KVO
- 2. KVO 的基本使用
?2.1 注冊方法
?2.2 監聽方法
?2.3 移除方法
?2.4 使用示例
?2.5 實際應用
?2.6 KVO 觸發監聽方法的方式
??2.6.1 自動觸發
??2.6.2 手動觸發- 3. KVO 的進階使用
?3.1 observationInfo 屬性
?3.2 context 的使用
?3.3 KVO 監聽集合對象
?3.4 KVO 的自動觸發控制
?3.5 KVO 的手動觸發
?3.6 KVO 新舊值相等時不觸發
?3.7 KVO 手動觀察集合屬性
?3.8 KVO 的依賴觀察
??3.8.1 一對一關系
??3.8.2 一對多關系- 4. KVO 的使用注意
?4.1 移除觀察者的注意點
?4.2 防止多次注冊和移除相同的 KVO
?4.3 其它注意點- 5. KVO 的實現原理
?5.1 isa-swizzling
?5.2 KVO 動態生成的子類都有哪些方法- 6. FBKVOController
?6.1 系統 KVO 的缺點
?6.2 FBKVOController 的介紹
?6.3 FBKVOController 的優點
?6.4 FBKVOController 的使用
?6.5 FBKVOController 的解析- 參考
1. 什么是 KVO
-
KVO
的全稱是Key-Value Observing
,俗稱“鍵值觀察/監聽”,是蘋果提供的一套事件通知機制,允許一個對象觀察/監聽另一個對象指定屬性值的改變。當被觀察對象屬性值發生改變時,會觸發KVO
的監聽方法來通知觀察者。KVO
是在MVC
應用程序中的各層之間進行通信的一種特別有用的技術。 -
KVO
和NSNotification
都是iOS
中觀察者模式的一種實現。 -
KVO
可以監聽單個屬性的變化,也可以監聽集合對象的變化。監聽集合對象變化時,需要通過KVC
的mutableArrayValueForKey:
等可變代理方法獲得集合代理對象,并使用代理對象進行操作,當代理對象的內部對象發生改變時,會觸發KVO
的監聽方法。集合對象包含NSArray
和NSSet
。 -
KVO
和KVC
有著密切的關系,如果想要深入了解KVO
,建議先學習KVC
。
傳送門:iOS - 關于 KVC 的一些總結
2. KVO 的基本使用
KVO
使用三部曲:添加/注冊KVO
監聽、實現監聽方法以接收屬性改變通知、 移除KVO
監聽。
- 調用方法
addObserver:forKeyPath:options:context:
給被觀察對象添加觀察者; - 在觀察者類中實現
observeValueForKeyPath:ofObject:change:context:
方法以接收屬性改變的通知消息; - 當觀察者不需要再監聽時,調用
removeObserver:forKeyPath:
方法將觀察者移除。需要注意的是,至少需要在觀察者銷毀之前,調用此方法,否則可能會導致Crash
。
2.1 注冊方法
/*
** target: 被觀察對象
** observer:觀察者對象
** keyPath: 被觀察對象的屬性的關鍵路徑,不能為nil
** options: 觀察的配置選項,包括觀察的內容(枚舉類型):
NSKeyValueObservingOptionNew:觀察新值
NSKeyValueObservingOptionOld:觀察舊值
NSKeyValueObservingOptionInitial:觀察初始值,如果想在注冊觀察者后,立即接收一次回調,可以加入該枚舉值
NSKeyValueObservingOptionPrior:分別在值改變前后觸發方法(即一次修改有兩次觸發)
** context: 可以傳入任意數據(任意類型的對象或者C指針),在監聽方法中可以接收到這個數據,是KVO中的一種傳值方式
如果傳的是一個對象,必須在移除觀察之前持有它的強引用,否則在監聽方法中訪問context就可能導致Crash
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
2.2 監聽方法
如果對象被注冊成為觀察者,則該對象必須能響應以下監聽方法,即該對象所屬類中必須實現監聽方法。當被觀察對象屬性發生改變時就會調用監聽方法。如果沒有實現就會導致Crash
。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/*
** keyPath:被觀察對象的屬性的關鍵路徑
** object: 被觀察對象
** change: 字典 NSDictionary<NSKeyValueChangeKey, id>,屬性值更改的詳細信息,根據注冊方法中options參數傳入的枚舉來返回
key為 NSKeyValueChangeKey 枚舉類型
{
1.NSKeyValueChangeKindKey:存儲本次改變的信息(change字典中默認包含這個key)
{
對應枚舉類型 NSKeyValueChange
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
如果是對被觀察對象屬性(包括集合)進行賦值操作,kind 字段的值為 NSKeyValueChangeSetting
如果被觀察的是集合對象,且進行的是(插入、刪除、替換)操作,則會根據集合對象的操作方式來設置 kind 字段的值
插入:NSKeyValueChangeInsertion
刪除:NSKeyValueChangeRemoval
替換:NSKeyValueChangeReplacement
}
2.NSKeyValueChangeNewKey:存儲新值(如果options中傳入NSKeyValueObservingOptionNew,change字典中就會包含這個key)
3.NSKeyValueChangeOldKey:存儲舊值(如果options中傳入NSKeyValueObservingOptionOld,change字典中就會包含這個key)
4.NSKeyValueChangeIndexesKey:如果被觀察的是集合對象,且進行的是(插入、刪除、替換)操作,則change字典中就會包含這個key
這個key的value是一個NSIndexSet對象,包含更改關系中的索引
5.NSKeyValueChangeNotificationIsPriorKey:如果options中傳入NSKeyValueObservingOptionPrior,則在改變前通知的change字典中會包含這個key。
這個key對應的value是NSNumber包裝的YES,我們可以這樣來判斷是不是在改變前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES]
}
** context:注冊方法中傳入的context
*/
}
2.3 移除方法
在調用注冊方法后,KVO
并不會對觀察者進行強引用,所以需要注意觀察者的生命周期。至少需要在觀察者銷毀之前,調用以下方法移除觀察者,否則如果在觀察者被釋放后,再次觸發KVO
監聽方法就會導致Crash
。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
2.4 使用示例
以下使用KVO
為person
對象添加觀察者為當前viewController
,監聽person
對象的name
屬性值的改變。當name
值改變時,觸發KVO
的監聽方法。
- (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 實際應用
KVO
主要用來做鍵值觀察操作,想要一個值發生改變后通知另一個對象,則用KVO
實現最為合適。斯坦福大學的iOS
教程中有一個很經典的案例,通過KVO
在Model
和Controller
之間進行通信。如圖所示:
2.6 KVO觸發監聽方法的方式
KVO
觸發分為自動觸發和手動觸發兩種方式。
2.6.1 自動觸發
① 如果是監聽對象特定屬性值的改變,通過以下方式改變屬性值會觸發KVO
:
- 使用點語法
- 使用
setter
方法 - 使用
KVC
的setValue:forKey:
方法 - 使用
KVC
的setValue:forKeyPath:
方法
② 如果是監聽集合對象的改變,需要通過KVC
的mutableArrayValueForKey:
等方法獲得代理對象,并使用代理對象進行操作,當代理對象的內部對象發生改變時,會觸發KVO
。集合對象包含NSArray
和NSSet
。
2.6.2 手動觸發
① 普通對象屬性或是成員變量使用:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
② NSArray
對象使用:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
③ NSSet
對象使用:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
3. KVO 的進階使用
3.1 observationInfo 屬性
-
observationInfo
屬性是NSKeyValueObserving.h
文件中系統通過分類給NSObject
添加的屬性,所以所有繼承于NSObject
的對象都含有該屬性; - 可以通過
observationInfo
屬性查看被觀察對象的全部觀察信息,包括observer
、keyPath
、options
、context
等。
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
3.2 context 的使用
注冊方法addObserver:forKeyPath:options:context:
中的context
可以傳入任意數據,并且可以在監聽方法中接收到這個數據。
context
作用:標簽-區分,可以更精確的確定被觀察對象屬性,用于繼承、 多監聽;也可以用來傳值。
??KVO
只有一個監聽回調方法observeValueForKeyPath:ofObject:change:context:
,我們通常情況下可以在注冊方法中指定context
為NULL
,并在監聽方法中通過object
和keyPath
來判斷觸發KVO
的來源。
??但是如果存在繼承的情況,比如現在有 Person 類和它的兩個子類 Teacher 類和 Student 類,person、teacher 和 student 實例對象都對 account 對象的 balance 屬性進行觀察。問題:
??① 當 balance 發生改變時,應該由誰來處理呢?
??② 如果都由 person 來處理,那么在 Person 類的監聽方法中又該怎么判斷是自己的事務還是子類對象的事務呢?
??這時候通過使用context
就可以很好地解決這個問題,在注冊方法中為context
設置一個獨一無二的值,然后在監聽方法中對context
值進行檢驗即可。蘋果的推薦用法:用
context
來精確的確定被觀察對象屬性,使用唯一命名的靜態變量的地址作為context
的值。可以為整個類設置一個context
,然后在監聽方法中通過object
和keyPath
來確定被觀察屬性,這樣存在繼承的情況就可以通過context
來判斷;也可以為每個被觀察對象屬性設置不同的context
,這樣使用context
就可以精確的確定被觀察對象屬性。
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
優點:嵌套少、性能高、更安全、擴展性強。 -
context
注意點:
① 如果傳的是一個對象,必須在移除觀察之前持有它的強引用,否則在監聽方法中訪問context
就可能導致Crash
;
② 空傳NULL
而不應該傳nil
。
3.3 KVO 監聽集合對象
KVO
可以監聽單個屬性的變化,也可以監聽集合對象的變化。監聽集合對象變化時,需要通過KVC
的mutableArrayValueForKey:
等方法獲得代理對象,并使用代理對象進行操作,當代理對象的內部對象發生改變時,會觸發KVO
的監聽方法。集合對象包含NSArray
和NSSet
。
(注意:如果直接對集合對象進行操作改變,不會觸發KVO
。)
示例代碼及輸出如下:
觀察者 viewController 對被觀察對象 person 的 mArray 屬性進行監聽。
- (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"]; //如果直接對數組進行操作,不會觸發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:對應的值為數組操作的詳細信息,包括索引等
kind: 對應的值為數組操作的方式:
2:代表插入操作
3:代表刪除操作
4:代表替換操作
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
new/old:如果是插入操作,則字典中只會有new字段,對應的值為插入的元素,前提條件是options中傳入了(NSKeyValueObservingOptionNew)
如果是刪除操作,則字典中只會有old字段,對應的值為刪除的元素,前提條件是options中傳入了(NSKeyValueObservingOptionOld)
如果是替換操作,則字典中new和old字段都可以存在,對應的值為替換后的元素和替換前的元素,前提條件是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 的自動觸發控制
??可以在被觀察對象的類中重寫+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
方法來控制KVO
的自動觸發。
??如果我們只允許外界觀察 person 的 name 屬性,可以在 Person 類如下操作。這樣外界就只能觀察 name 屬性,即使外界注冊了對 person 對象其它屬性的監聽,那么在屬性發生改變時也不會觸發KVO
。
// 返回值代表允不允許觸發 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"name"]) {
automatic = YES;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
??也可以實現遵循命名規則為+ (BOOL)automaticallyNotifiesObserversOf<Key>
的方法來單一控制屬性的KVO
自動觸發,<Key>
為屬性名(首字母大寫)。
+ (BOOL)automaticallyNotifiesObserversOfName
{
return NO;
}
注意:
- 第一個方法的優先級高于第二個方法。如果實現了
automaticallyNotifiesObserversForKey:
方法,并對<Key>
做了處理,則系統就不會再調用該<Key>
的automaticallyNotifiesObserversOf<Key>
方法。options
指定的NSKeyValueObservingOptionInitial
觸發的KVO
通知,是無法被automaticallyNotifiesObserversForKey:
阻止的。
3.5 KVO 的手動觸發
使用場景:
- 使用
KVO
監聽成員變量值的改變; - 在某些需要控制監聽過程的場景下。比如:為了盡量減少不必要的觸發通知操作,或者當多個更改同時具備的時候才調用屬性改變的監聽方法。
??由于KVO
的本質,重寫setter
方法來達到可以通知所有觀察者對象的目的,所以只有通過setter
方法或KVC
方法去修改屬性變量值的時候,才會觸發KVO
,直接修改成員變量不會觸發KVO
。
??當我們要使用KVO
監聽成員變量值改變的時候,可以通過在為成員變量賦值的前后手動調用willChangeValueForKey:
和didChangeValueForKey:
兩個方法來手動觸發KVO
,如:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person willChangeValueForKey:@"age"];
self.person->_age = 18;
[self.person didChangeValueForKey:@"age"];
}
??NSKeyValueObservingOptionPrior
(分別在值改變前后觸發方法,即一次修改有兩次觸發)的兩次觸發分別在willChangeValueForKey:
和didChangeValueForKey:
的時候進行的。
??如果注冊方法中options
傳入NSKeyValueObservingOptionPrior
,那么可以通過只調用willChangeValueForKey:
來觸發改變前的那次KVO
,可以用于在屬性值即將更改前做一些操作。
3.6 KVO 新舊值相等時不觸發
??有時候我們可能會有這樣的需求,KVO
監聽的屬性值修改前后相等的時候,不觸發KVO
的監聽方法,可以結合KVO
的自動觸發控制和手動觸發來實現。
??例如:對 person 對象的 name 屬性注冊了KVO
監聽,我們希望在對 name 屬性賦值時做一個判斷,如果新值和舊值相等,則不觸發KVO
,可以在 Person 類中如下這樣實現,將 name 屬性值改變的KVO
觸發方式由自動觸發改為手動觸發。
+ (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 手動觀察集合屬性
有些情況下我們想手動觀察集合屬性,下面以觀察數組為例。
關鍵方法:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
需要注意的是,根據KVC
的NSMutableArray 搜索模式
:
傳送門:iOS - 關于 KVC 的一些總結
- 至少要實現一個插入和一個刪除方法,否則不會觸發
KVO
。如
插入方法:insertObject:in<Key>AtIndex:
或insert<Key>:atIndexes:
刪除方法:removeObjectFrom<Key>AtIndex:
或remove<Key>AtIndexes:
- 可以不實現替換方法,但是如果不實現替換方法,執行替換操作時,
KVO
會把它當成先刪除后添加,即會觸發兩次KVO
。第一次觸發的KVO
中change
字典的old
鍵的值為替換前的元素,第二次觸發的KVO
中change
字典的new
鍵的值為替換后的元素,前提條件是注冊方法中的options
傳入對應的枚舉值。 - 如果實現替換方法,則執行替換操作只會觸發一次
KVO
,并且change
字典會同時包含new
和old
,前提條件是注冊方法中的options
傳入對應的枚舉值。
替換方法:replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
- 建議實現替換方法以提高性能。
示例代碼如下:
+ (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 一對一關系
??有些情況下,一個屬性的改變依賴于別的一個或多個屬性的改變,也就是說當別的屬性改了,這個屬性也會跟著改變。
??比如我們想要對 Download 類中的 downloadProgress 屬性進行KVO
監聽,該屬性的改變依賴于 writtenData 和 totalData 屬性的改變。觀察者監聽了 downloadProgress ,當 writtenData 和 totalData 屬性值改變時,觀察者也應該被通知。以下有兩種方法可以解決這個問題。
- 重寫以下方法來指明 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;
}
- 實現一個遵循命名規則為
keyPathsForValuesAffecting<Key>
的類方法,<Key>
是依賴于其他值的屬性名(首字母大寫):
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress
{
return [NSSet setWithObjects:@"writtenData",@"totalData", nil];
}
注意:以上兩個方法可以同時存在,且都會調用,但是最終結果會以
keyPathsForValuesAffectingValueForKey:
為準。
3.8.2 一對多關系
??以上方法在觀察集合屬性時就不管用了。例如,假如你有一個 Department 類,它有一個裝有 Employee 類的實例對象的數組,Employee 類有 salary 屬性。你希望 Department 類有一個 totalSalary 屬性來計算所有員工的薪水,也就是在這個關系中 Department 的 totalSalary 依賴于所有 Employee 實例對象的 salary 屬性。以下有兩種方法可以解決這個問題。
- 你可以用
KVO
將 parent(比如 Department )作為所有 children(比如 Employee )相關屬性的觀察者。你必須在把 child 添加或刪除到 parent 時把 parent 作為 child 的觀察者添加或刪除。在observeValueForKeyPath:ofObject:change:context:
方法中我們可以針對被依賴項的變更來更新依賴項的值:
#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
中觀察者模式的另一種實現方式:通知 (NSNotification
) 。
4. KVO的使用注意
4.1 移除觀察者的注意點
- 在調用
KVO
注冊方法后,KVO
并不會對觀察者進行強引用,所以需要注意觀察者的生命周期。至少需要在觀察者銷毀之前,調用KVO
移除方法移除觀察者,否則如果在觀察者被釋放后,再次觸發KVO
監聽方法就會導致Crash
。 -
KVO
的注冊方法和移除方法應該是成對的,如果重復調用移除方法,就會拋出異常NSRangeException
并導致程序Crash
。 - 蘋果官方推薦的方式是,在觀察者初始化期間(
init
或者viewDidLoad
的時候)注冊為觀察者,在釋放過程中(dealloc
時)調用移除方法,這樣可以保證它們是成對出現的,是一種比較理想的使用方式。
4.2 防止多次注冊和移除相同的KVO
??有時候我們難以避免多次注冊和移除相同的KVO
,或者移除了一個未注冊的觀察者,從而產生可能會導致Crash
的風險。
??三種解決方案:黑科技防止多次添加刪除KVO出現的問題
- 利用
@try @catch
(只能針對刪除多次KVO
的情況下)
給NSObject
增加一個分類,然后利用Runtime API
交換系統的removeObserver
方法,在里面添加@try @catch
; - 利用 模型數組 進行存儲記錄;
- 利用
observationInfo
里私有屬性。
4.3 其它注意點
- 如果對象被注冊成為觀察者,則該對象必須能響應監聽方法,即該對象所屬類中必須實現監聽方法。當被觀察對象屬性發生改變時就會調用監聽方法。如果沒有實現就會導致
Crash
。所以KVO
三部曲缺一不可。 -
keyPath
傳入的是一個字符串,為避免寫錯,可以使用NSStringFromSelector(@selector(propertyName))
,將屬性的getter
方法SEL
轉換成字符串,在編譯階段對keyPath
進行檢驗。 - 如果注冊方法中
context
傳的是一個對象,必須在移除觀察之前持有它的強引用,否則在監聽方法中訪問context
就可能導致Crash
。- 可以使用
__bridge_retained
橋接剝奪對象的內存管理權,但必須記得在不需要該對象時釋放它,否則內存泄露。關于橋接可以參閱《iOS - 老生常談內存管理(三):ARC 面世 —— Toll-Free Bridging》; - 或者使用全局變量。
- 可以使用
- 如果是監聽集合對象的改變,需要通過
KVC
的mutableArrayValueForKey:
等方法獲得代理對象,并使用代理對象進行操作,當代理對象的內部對象發生改變時,會觸發KVO
。如果直接對集合對象進行操作改變,不會觸發KVO
。 - 在觀察者類的監聽方法中,應該為無法識別的
context
或者object
、keyPath
調用父類的實現[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
。
5. KVO的實現原理
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.
??以上是蘋果官方對KVO
實現的解釋,只說明了KVO
是使用isa-swizzling
技術來實現的,并沒有做過多介紹。
5.1 isa-swizzling
??蘋果使用了isa
混寫技術(isa-swizzling
)來實現KVO
。當我們調用了addObserver:forKeyPath:options:context:
方法,為instance
被觀察對象添加KVO
監聽后,系統會在運行時利用Runtime API
動態創建instance
對象所屬類A
的子類NSKVONotifying_A
,并且讓instance
對象的isa
指向這個全新的子類,并重寫原類A
的被觀察屬性的setter
方法來達到可以通知所有觀察者對象的目的。
??這個子類的isa
指針指向它自己的meta-class
對象,而不是原類的meta-class
對象。
??重寫的setter
方法的SEL
對應的IMP
為Foundation
中的_NSSetXXXValueAndNotify
函數(XXX
為Key
的數據類型),當被觀察對象的屬性發送改變時,會調用_NSSetXXXValueAndNotify
函數,這個函數中會調用:
-
willChangeValueForKey:
方法 - 父類原來的
setter
方法 -
didChangeValueForKey:
方法(內部會觸發監聽器即觀察對象observer
的監聽方法:observeValueForKeyPath:ofObject:change:context:
)
??在移除KVO
監聽后,被觀察對象的isa
會指回原類A
,但是NSKVONotifying_A
類并沒有銷毀,還保存在內存中。
5.2 KVO 動態生成的子類都有哪些方法
??NSKVONotifying_A
除了重寫了setter
方法,還重寫了class
、dealloc
、_isKVOA
這三個方法(可以使用runtime
的class_copyMethodList
函數打印方法列表獲得),其中:
-
class
:class
方法中返回的是父類的class
對象,目的是為了不讓外界知道KVO
動態生成類的存在; -
dealloc
:釋放KVO
使用過程中產生的東西; -
_isKVOA
:用來標志它是一個KVO
的類。
6. FBKVOController
6.1 系統 KVO 的缺點
- 使用比較麻煩,需要三個步驟:添加/注冊
KVO
監聽、實現監聽方法以接收屬性改變通知、 移除KVO
監聽,缺一不可; - 需要手動移除觀察者,移除觀察者的時機必須合適,還不能重復移除;
- 注冊觀察者的代碼和事件發生處的代碼上下文不同,傳遞上下文
context
是通過void *
指針; - 需要實現
-observeValueForKeyPath:ofObject:change:context:
方法,比較麻煩; - 在復雜的業務邏輯中,準確判斷被觀察者相對比較麻煩,有多個被觀測的對象和屬性時,需要在方法中寫大量的
if
進行判斷。
6.2 FBKVOController 的介紹
FBKVOController
是 Facebook 開源的一個基于系統KVO
實現的框架。支持Objective-C
和Swift
語言。
GitHub:https://github.com/facebook/KVOController
6.3 FBKVOController 的優點
- 會自動移除觀察者;
- 函數式編程,可以一行代碼實現系統
KVO
的三個步驟; - 實現
KVO
與事件發生處的代碼上下文相同,不需要跨方法傳參數; - 增加了
block
和SEL
自定義操作對NSKeyValueObserving
回調的處理支持; - 每一個
keyPath
會對應一個block
或者SEL
,不需要使用if
判斷keyPath
; - 可以同時對一個對象的多個屬性進行監聽,寫法簡潔;
- 線程安全。
6.4 FBKVOController 的使用
FBKVOController
實現了觀察者和被觀察者的角色反轉,系統的KVO
是被觀察者添加觀察者,而FBKVO
實現了觀察者主動去添加被觀察者,實現了角色上的反轉,使用比較方便。
// 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 的解析
如何優雅地使用KVO(簡書)
iOS - FBKVOController 實現原理(簡書)
參考
Key-Value Observing Programming Guide(蘋果官方文檔)
iOS - 關于 KVC 的一些總結(簡書)
KVO原理分析及使用進階(簡書)
iOS開發 - 黑科技防止多次添加刪除KVO出現的問題(簡書)
談談 KVO(簡書)
GitHub/facebook/KVOController(GitHub)
如何優雅地使用KVO(簡書)
iOS - FBKVOController 實現原理(簡書)