KVO
KVO 是 Key-Value-Observing 的簡(jiǎn)稱董虱。
KVO 是一個(gè)觀察者模式申鱼。觀察一個(gè)對(duì)象的屬性,注冊(cè)一個(gè)指定的路徑淫半,若這個(gè)對(duì)象的的屬性被修改匣砖,則 KVO 會(huì)自動(dòng)通知觀察者。
更通俗的話來說就是任何對(duì)象都允許觀察其他對(duì)象的屬性对人,并且可以接收其他對(duì)象狀態(tài)變化的通知规伐。
KVO 基本使用
1.// 注冊(cè)觀察者匣缘,實(shí)施監(jiān)聽;
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
2.// 回調(diào)方法培慌,在這里處理屬性發(fā)生的變化柑爸;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
3.// 移除觀察者;
[self removeObserver:self forKeyPath:@“age"];
KVO 在 Apple 中的 API 文檔如下:
Automatic key-value observing is implemented using a technique called isa-swizzling… 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 …
Apple 使用了 isa 攪拌技術(shù)(isa-swizzling)來實(shí)現(xiàn)的 KVO 馅而。當(dāng)一個(gè)觀察者注冊(cè)對(duì)象的一個(gè)屬性 isa 觀察對(duì)象的指針被修改瓮恭,指著一個(gè)中間類而不是在真正的類厘熟。
isa 指針的作用:每個(gè)對(duì)象都有 isa 指針维哈,指向該對(duì)象的類阔挠,它告訴 runtime 系統(tǒng)這個(gè)對(duì)象的類是什么脑蠕。
注:如果對(duì) runtime 不很清楚的話可以看下這篇文章Objective-C 中的 Runtime
舉個(gè)栗子:
_person = [[Person alloc] init];
/**
* 添加觀察者
*
* @param observer 觀察者
* @param keyPath 被觀察的屬性名稱
* @param options 觀察屬性的新值谴仙、舊值等的一些配置(枚舉值,可以根據(jù)需要設(shè)置锁摔,例如這里可以使用兩項(xiàng))
* @param context 上下文哼审,可以為nil。
*/
[_person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
/**
* KVO回調(diào)方法
*
* @param keyPath 被修改的屬性
* @param object 被修改的屬性所屬對(duì)象
* @param change 屬性改變情況(新舊值)
* @param context context傳過來的值
*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
{
NSLog(@"%@對(duì)象的%@屬性改變了:%@",object,keyPath,change);
}
/**
* 移除觀察者
*/
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"age"];
}
KVO 實(shí)現(xiàn)原理
當(dāng)某個(gè)類的對(duì)象第一次被觀察時(shí)十气,系統(tǒng)就會(huì)在運(yùn)行期動(dòng)態(tài)地創(chuàng)建該類的一個(gè)派生類砸西,在這個(gè)派生類中重寫基類中任何被觀察屬性的 setter 方法址儒。 派生類在被重寫的 setter 方法實(shí)現(xiàn)真正的通知機(jī)制,就如前面手動(dòng)實(shí)現(xiàn)鍵值觀察那樣鸳慈。這么做是基于設(shè)置屬性會(huì)調(diào)用 setter 方法喧伞,而通過重寫就獲得了 KVO 需要的通知機(jī)制。當(dāng)然前提是要通過遵循 KVO 的屬性設(shè)置方式來變更屬性值翁逞,如果僅是直接修改屬性對(duì)應(yīng)的成員變量挖函,是無法實(shí)現(xiàn) KVO 的彼念。 同時(shí)派生類還重寫了 class 方法以“欺騙”外部調(diào)用者它就是起初的那個(gè)類。然后系統(tǒng)將這個(gè)對(duì)象的 isa 指針指向這個(gè)新誕生的派生類哲思,因此這個(gè)對(duì)象就成為該派生類的對(duì)象了吩案,因而在該對(duì)象上對(duì) setter 的調(diào)用就會(huì)調(diào)用重寫的 setter,從而激活鍵值通知機(jī)制靠益。此外残揉,派生類還重寫了 dealloc 方法來釋放資源。
派生類 NSKVONotifying_Person 剖析:
在這個(gè)過程壳快,被觀察對(duì)象的 isa 指針從指向原來的 Person 類镇草,被 KVO 機(jī)制修改為指向系統(tǒng)新創(chuàng)建的子類 NSKVONotifying_Person 類,來實(shí)現(xiàn)當(dāng)前類屬性值改變的監(jiān)聽竖伯。
所以當(dāng)我們從應(yīng)用層面上看來因宇,完全沒有意識(shí)到有新的類出現(xiàn),這是系統(tǒng)“隱瞞”了對(duì) KVO 的底層實(shí)現(xiàn)過程本姥,讓我們誤以為還是原來的類杭棵。但是此時(shí)如果我們創(chuàng)建一個(gè)新的名為 NSKVONotifying_Person 的類(),就會(huì)發(fā)現(xiàn)系統(tǒng)運(yùn)行到注冊(cè) KVO 的那段代碼時(shí)程序就崩潰先舷,因?yàn)橄到y(tǒng)在注冊(cè)監(jiān)聽的時(shí)候動(dòng)態(tài)創(chuàng)建了名為 NSKVONotifying_Person 的中間類滓侍,并指向這個(gè)中間類了互躬。
因而在該對(duì)象上對(duì) setter 的調(diào)用就會(huì)調(diào)用已重寫的 setter谣蠢,從而激活鍵值通知機(jī)制。這也是 KVO 回調(diào)機(jī)制裂逐,為什么都俗稱 KVO 技術(shù)為黑魔法的原因之一吧:內(nèi)部神秘泣栈、外觀簡(jiǎn)潔。
子類 setter 方法剖析:
KVO 在調(diào)用存取方法之前總是調(diào)用 willChangeValueForKey:掺涛,通知系統(tǒng)該 keyPath 的屬性值即將變更疼进。 當(dāng)改變發(fā)生后,didChangeValueForKey: 被調(diào)用矮燎,通知系統(tǒng)該 keyPath 的屬性值已經(jīng)變更赔癌。 之后,observeValueForKey:ofObject:change:context: 也會(huì)被調(diào)用峡谊。
重寫觀察屬性的 setter 方法這種方式是在運(yùn)行時(shí)而不是編譯時(shí)實(shí)現(xiàn)的既们。 KVO 為子類的觀察者屬性重寫調(diào)用存取方法的工作原理在代碼中相當(dāng)于:
- (void)setName:(NSString *)newName
{
[self willChangeValueForKey:@"name"]; // KVO在調(diào)用存取方法之前總調(diào)用
[super setValue:newName forKey:@"name"]; // 調(diào)用父類的存取方法
[self didChangeValueForKey:@"name"]; // KVO在調(diào)用存取方法之后總調(diào)用
}
總結(jié): KVO 的本質(zhì)就是監(jiān)聽對(duì)象的屬性進(jìn)行賦值的時(shí)候有沒有調(diào)用 setter 方法
- 系統(tǒng)會(huì)動(dòng)態(tài)創(chuàng)建一個(gè)繼承于 Person 的 NSKVONotifying_Person
- person 的 isa 指針指向的類 Person 變成 NSKVONotifying_Person啥纸,所以接下來的 person.age = newAge 的時(shí)候婴氮,他調(diào)用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子類)的 setter 方法
- 重寫
NSKVONotifying_Person的setter方法:[super setName:newName]
- 通知觀察者告訴屬性改變荣暮。
KVO 應(yīng)用
監(jiān)聽 ScrollView 的 contentOffSet 屬性罩驻,采取相應(yīng)的措施:
[scrollview addObserver:self
forKeyPath:@“contentOffset
options:NSKeyValueObservingOptionNew
context:nil];
下面是用 KVO 寫的一個(gè)通過監(jiān)聽 scrollview 的 contentOffSet 實(shí)現(xiàn)的一個(gè)小刷新功能,感興趣的可以看下骏啰。
KVO 總結(jié)
KVO 是一個(gè)對(duì)象能觀察另一個(gè)對(duì)象屬性的值抽高,KVO 適合任何對(duì)象監(jiān)聽另一個(gè)對(duì)象的改變,這是一個(gè)對(duì)象與另外一個(gè)對(duì)象保持同步的一種方法祈秕。KVO 只能對(duì)屬性做出反應(yīng)雏胃,不會(huì)用來對(duì)方法或者動(dòng)作做出反應(yīng)志鞍。
優(yōu)點(diǎn):
- 提供一個(gè)簡(jiǎn)單的方法來實(shí)現(xiàn)兩個(gè)對(duì)象的同步。
- 能夠提供觀察的屬性的新值和舊值统翩。
- 每一次屬性值改變都是自動(dòng)發(fā)送通知厂汗,不需要開發(fā)者手動(dòng)實(shí)現(xiàn)。
- 用 keypath 來觀察屬性娶桦,因此也可以觀察嵌套對(duì)象汁汗。
缺點(diǎn):
- 觀察的屬性必須使用字符串來定義,因此編譯器不會(huì)出現(xiàn)警告和檢查
- 只能重寫回調(diào)方法來后去通知祈争,不能自定義 selector角寸。當(dāng)觀察多個(gè)對(duì)象的屬性時(shí)就要寫"if"語句,來判斷當(dāng)前的回調(diào)屬于哪個(gè)對(duì)象的屬性的回調(diào)墨吓。