前言
KVO 是日常 iOS 開(kāi)發(fā)中經(jīng)常使用的技術(shù),通過(guò)它可以很方便的對(duì)屬性進(jìn)行監(jiān)聽(tīng),本文不涉及底層原理,主要介紹以上方式在業(yè)務(wù)層的應(yīng)用,并總結(jié)各自的特點(diǎn),重點(diǎn)可以關(guān)注下 Swift KeyPath 的使用以及 ReactiveCocoa 封裝的接口, 我們的項(xiàng)目正在逐步轉(zhuǎn)為 Swift, 所以在我們項(xiàng)目中一般會(huì)用到的以下四種
- Foundation KVO
- ReactiveObjC KVO
- Swift KeyPath
- ReactiveCocoa KVO
1. Foundation KVO
由 Foundation 提供的 KVO 機(jī)制是絕大多數(shù) KVO 接口的底層基礎(chǔ),它的大致實(shí)現(xiàn)方式可以參考 深入理解 ObjC 中的第 2 點(diǎn)
Example Code:
deinit {
// 對(duì)象釋放時(shí)必須手動(dòng)移除監(jiān)聽(tīng)
removeObserver(self, forKeyPath: #keyPath(model.currentValue))
}
override func setupObserver() {
super.setupObserver()
// 監(jiān)聽(tīng) model 的 currentValue 屬性
addObserver(self, forKeyPath: #keyPath(model.currentValue), options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
// 監(jiān)聽(tīng)到變動(dòng),可以從 change 中獲取初始值/舊值/新值
}
優(yōu)點(diǎn):
- 通用性強(qiáng),不依賴其他框架
缺點(diǎn):
- key path 只能使用字符串硬編碼,無(wú)法感知被監(jiān)聽(tīng)的屬性名變動(dòng)
- 需要手動(dòng)移除,否則對(duì)象釋放時(shí)可能導(dǎo)致 crash
- 對(duì)于一對(duì)一的監(jiān)聽(tīng)及回調(diào),會(huì)使邏輯分散,使用起來(lái)不如 block based 的接口方便
2. ReactiveObjC KVO
這個(gè)方法是我們項(xiàng)目中應(yīng)用最多的一種,底層基于 Foundation KVO,提供了一個(gè) RACObserve 宏,將一次監(jiān)聽(tīng)抽象為一個(gè)信號(hào)
Example Code:
@weakify(self);
[RACObserve(self.model, currentValue) subscribeNext:^(id _Nullable x) {
@strongify(self);
// 監(jiān)聽(tīng)到屬性變動(dòng),x 為最新值
}];
優(yōu)點(diǎn):
- 解決了 key path 的硬編碼問(wèn)題
- 自動(dòng)綁定當(dāng)前對(duì)象的生命周期,無(wú)需手動(dòng)移除
- 使用 block base 接口,對(duì)于單一屬性監(jiān)聽(tīng)及處理非常友好
缺點(diǎn):
- 需要依賴 ReactiveObjC
- 回調(diào)中的類型被抹除
- 需要小心循環(huán)引用的問(wèn)題
3. Swift KeyPath
KeyPath 是 Swift 4 推出的一套用于 Swift 的 KVO 接口,它可以支持 key path 名的校驗(yàn),以及回調(diào)中的數(shù)據(jù)類型推導(dǎo)
注意:這個(gè) K 是大寫,另外一個(gè)叫 #keyPath 的東西本質(zhì)上是獲取對(duì)應(yīng) key path 的字符串(參見(jiàn) Foundation KVO 中的例子),與之類似作用的有 ReactiveObjC 中的 @keypath 宏
Example Code:
// 監(jiān)聽(tīng) model 的 currentValue 屬性
observation = model.observe(\.currentValue, options: .new, changeHandler: { [weak self] _, change in
// 訪問(wèn) change.newValue 即可獲取新的值,雖然類型可以推導(dǎo)出來(lái),但是 Optional 的
})
上面的 observe 方法還可以省略 options 參數(shù),這樣回調(diào)中的 change 將不包含屬性值,需要訪問(wèn)對(duì)象的屬性來(lái)獲取新值
文檔原文:newValue and oldValue will only be non-nil if .new/.old is passed to "observe()". In general, get the most up to date value by accessing it directly on the observed object instead.
Example Code 2:
observation = model.observe(\.currentValue) { [weak self] _, _ in
// 訪問(wèn) model.currentValue 獲取新值
}
可以發(fā)現(xiàn)這個(gè) observe 方法返回來(lái)一個(gè) NSKeyValueObservation 對(duì)象,那么這個(gè)返回對(duì)象可以像 RACDisposable 一樣不理會(huì)嗎?
答案是否定的,根據(jù)文檔顯示:
when the returned NSKeyValueObservation is deinited or invalidated, it will stop observing
很明顯,如果這個(gè)返回的 NSKeyValueObservation 對(duì)象被釋放,那監(jiān)聽(tīng)也就結(jié)束了,所以 observe 方法也沒(méi)有標(biāo)記 @discardableResult,如果你不接收返回值,將得到警告
優(yōu)點(diǎn):
- 針對(duì) Swift 調(diào)用優(yōu)化,支持類型推導(dǎo)
缺點(diǎn):
- 只支持 Swift
- 需要再維護(hù)一個(gè) observation
- 被監(jiān)聽(tīng)的類需要是 objc 的,且屬性需要標(biāo)記為 dynamic
4. ReactiveCocoa KVO
ReactiveCocoa 的使用邏輯與 ReactiveObjC 類似,都將一次監(jiān)聽(tīng)抽象為一個(gè)信號(hào)。內(nèi)部封裝了 Foundation KVO 和Swift KeyPath 兩種實(shí)現(xiàn)
基于 Foundation KVO 的接口調(diào)用示例:
// 監(jiān)聽(tīng) model 的 currentValue 屬性
model.reactive.signal(forKeyPath: "currentValue").observeValues { [weak self] in
// 類型無(wú)法推斷,需要進(jìn)行解包操作
}
基于 Swift KeyPath 的接口調(diào)用示例:
model.reactive.signal(for: \.currentValue).observeValues { [weak self] in
// 類型可被自動(dòng)推斷,無(wú)需解包
}
可以發(fā)現(xiàn)它的完成度是相當(dāng)高了,監(jiān)聽(tīng)的對(duì)象、屬性名、屬性類型,都支持編譯器校驗(yàn),還幫我們做好了解包,最關(guān)鍵的是無(wú)需維護(hù) observation 了
優(yōu)點(diǎn):
Swift 調(diào)用友好
類型推導(dǎo)最完善
缺點(diǎn):
- 需要依賴 ReactiveCocoa & ReactiveSwift
5. Summary
| KVO 方式 | 外部依賴 | 生命周期管理 | key path | 回調(diào)中屬性類型推導(dǎo) | ObjC | Swift |
|---|---|---|---|---|---|---|
| Foundation KVO | 無(wú) | 手動(dòng)停止監(jiān)聽(tīng) | 字符串硬編碼(Swift #keyPath) | NO | YES | YES |
| Swift KVO | 無(wú) | 綁定 observation 對(duì)象 | 使用 \ 操作符 | YES (但被退化為 Optional) | NO | YES |
| ReactiveObjC KVO | ReactiveObjC | 自動(dòng)管理 | RACObserve 自帶 @keypath | NO | YES | NO |
| ReactiveCocoa KVO | ReactiveCocoa | 自動(dòng)管理 | 支持字符串或 \ 操作符 | YES(基于 KeyPath 的接口) | NO | YES |
文/ 夏天然后