KVO 簡介
KVO 鍵值觀察機(jī)制盆犁,就是觀察指定對象的指定屬性變化情況渐裸。
KVO 鍵值觀察 依賴于 KVC 健值編碼
Key-value observing 通常用于 MVC 中,model 與 controller直接的通訊掘譬。
繼承于 NSObject 才可以擁有 KVO 機(jī)制
兩種 KVO 應(yīng)用方式
- 自動 KVO
- 手動 KVO
舉個例子,一個 Person
類, 一個 Account
類
- [圖1]
當(dāng) Account
的屬性 余額 balance
改變時通知 Person
給 Account
添加觀察者 Person
[account addObserver:person forKeyPath:@"balance" options:options context:context];
- [圖2]
Person
觀察 Account
的屬性變化
Person
中實現(xiàn)觀察者方法 observeValueForKeyPath:ofObject:change:context:
- [圖3]
Account
移除觀察者 Person
[account removeObserver:person forKeyPath:@"balance" context:context]
- [圖4]
這篇文章的重要內(nèi)容
Registering for Key-Value Observing 注冊鍵值觀察的過程
Registering Dependent Keys 對于存在 key 依賴的鍵值觀察
Key-Value Observing Implementation Details 鍵值觀察的實現(xiàn)細(xì)節(jié)
1. Registering for Key-Value Observing 注冊鍵值觀察的過程
KVO 生命周期的過程欣舵,必須完成下面這三個方法
- 給被觀察者注冊觀察者
addObserver:forKeyPath:options:context:.
- 在觀察者里實現(xiàn)方法 接受通知
observeValueForKeyPath:ofObject:change:context:
- 移除觀察者
removeObserver:forKeyPath:
要在觀察者的內(nèi)存銷毀之前 移除觀察者機(jī)制
- 注冊觀察者 addObserver:forKeyPath:options:context:.
-
Options
Options 影響方法observeValueForKeyPath:ofObject:change:context:
中的change
字典-
NSKeyValueObservingOptionOld
:change
包含 old value -
NSKeyValueObservingOptionNew
: 'change' 包含 new value -
NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew
: 即包含 old 也包含 new -
NSKeyValueObservingOptionInitial
:change
中不包含 key 的值,會在 kvo 注冊的時候立即發(fā)送通知缀磕。 -
NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew
: 注冊kvo時立即發(fā)送通知change
中有 new 值缘圈,這里的 new 值是注冊之前 key 的值。 -
NSKeyValueObservingOptionPrior
: 會在值發(fā)生改變前發(fā)出一次通知袜蚕,當(dāng)然改變后的通知依舊還會發(fā)出糟把,也就是每次change都會有兩個通知。值變化之前發(fā)送通知的change
中包含一個鍵值對NSKeyValueChangeNotificationIsPriorKey:@(1)
, 值發(fā)生變化之后的的通知change
不包含上面提到的 鍵值對, 可以跟willChange
手動通知搭配使用
- 具體解釋查看文章: KVO Options 詳細(xì)介紹
-
-
Context
是一個 void * 指針牲剃,可以傳任意數(shù)據(jù)進(jìn)去遣疯,可以防止父類跟子類同時對同一個 key 注冊觀察者造成的異常。可以采用下面的方法創(chuàng)建一個 Context
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext; static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
命名規(guī)范:
static void * 類名+屬性名+Context
- Receiving Notification of a Change 接收通知響應(yīng)
所有的觀察者必須實現(xiàn)方法 `observeValueForKeyPath:ofObject:change:context: message.`
-
keyPath
: 注冊 KVO 時的keyPath
-
object
: 被觀察者對象 -
change
: 根據(jù)注冊 KVO 時 option 不同而展示不同的內(nèi)容凿傅,change
中可能會包含keyPath
變化前后的值缠犀,對應(yīng)的鍵有NSKeyValueChangeOldKey
NSKeyValueChangeNewKey
keyPath
是個標(biāo)量或者 c 語言結(jié)構(gòu)體,會把這些包裝成NSNumber
或NSValue
,
當(dāng)
keyPath
是個容器狭归,change
中可能會包含這個容器inserted
,removed
,replaced
的情況 對應(yīng)的鍵有
- `NSKeyValueChangeInsertion`
- `NSKeyValueChangeRemoval`
- `NSKeyValueChangeReplacement`
keypath
是個NSIndexSet
:change
中可能會包含數(shù)組
- (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];
}
}
- Removing an Object as an Observer 移除觀察者
觀察者被移除之后就不會再接受到通知夭坪。
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
需要注意的地方
- 不要重復(fù)移除觀察者,會造成異常过椎,如果不知道這個觀察者是否已經(jīng)被移除室梅,可以在
try/catch
安全移除觀察者 - 在觀察者內(nèi)存銷毀之前從觀察者中釋放出來,不然會造成內(nèi)存異常
- 無法檢測一個對象是否處于觀察者模式中疚宇,
-
init
或者viewDidLoad
中添加觀察者 -
dealloc
中移除觀察者
-
Automatic Change Notification 自動 KVO 通知
NSObject
提供了自動健值更改通知的實現(xiàn)亡鼠,自動 KVO 通知依賴于 KVC 編碼機(jī)制獲取, KVC method敷待,和 集合代理(collection proxy)mutableArrayValueForKey
:
手動 KVO
當(dāng)你想要控制整個 KVO 的進(jìn)程可以采用手動 KVO, 比如減少不必要的通知间涵,比如將大量的通知控制到一個通知中。
手動 KVO 跟 自動 KVO 可以共存榜揖,比如同一個對象的同一個屬性勾哩,可以在父類里自動 KVO, 在子類里手動 KVO, 重寫方法 automaticallyNotifiesObserversForKey
即可
-
打開手動 KVO 的開關(guān)
automaticallyNotifiesObserversForKey
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"balance"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic; }
- 手動觸發(fā) KVO(一般是在 set 方法里)
willChangeValueForKey
didChangeValueForKey
- (void)setBalance:(double)theBalance { if (theBalance != _balance) { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; } }
- 也可以把多個觸發(fā) KVO 的 key 放到一起
- (void)setBalance:(double)theBalance { [self willChangeValueForKey:@"balance"]; [self willChangeValueForKey:@"itemChanged"]; _balance = theBalance; _itemChanged = _itemChanged+1; [self didChangeValueForKey:@"itemChanged"]; [self didChangeValueForKey:@"balance"]; }
- 容器內(nèi)部更改時抗蠢,采用采用手動 KVO(當(dāng)容器內(nèi)容修改時,會觸發(fā) KVO)
注意:根據(jù)容器變化的類型去設(shè)置對應(yīng)的NSKeyValueChange
- NSKeyValueChangeInsertion
- NSKeyValueChangeRemoval
- NSKeyValueChangeReplacement
針對容器對象的 KVO 思劳,需要借用 KVC 機(jī)制創(chuàng)建的 容器代理迅矛。
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes { [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; // Remove the transaction objects at the specified indexes. [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; }
- 手動觸發(fā) KVO(一般是在 set 方法里)
2.KVO - 注冊依賴 key
單個關(guān)系
重寫下面兩個方法中的一個即可
keyPathsForValuesAffectingValueForKey
-
keyPathsForValuesAffecting<Key>
用例如下+ (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]; }
多個關(guān)系
比如,有個部門類 Department
潜叛,有個員工類 Employees
, 每個員工的薪水都會影響這個部門的總薪水秽褒。
可以在 Department
添加、刪除 Employees
的時候威兜,給 Employees
注冊销斟、移除觀察者 Employees
用例如下
```
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
```
3. KVO 內(nèi)部實現(xiàn)細(xì)節(jié)
使用 isa-swizzling
原理
當(dāng) 對象的屬性注冊到觀察者中時,會創(chuàng)建一個中間類椒舵,重寫了被觀察屬性的 setter 方法蚂踊。