1买优、KVO簡(jiǎn)介
KVO
即Key-Value Observing
讹开,翻譯成是中文鍵值觀察
丸卷,是一種非正式的協(xié)議枕稀,它定義了對(duì)象之間觀察和通知狀態(tài)改變的機(jī)制,是觀察者模式的一種衍生。KVO通過(guò)對(duì)對(duì)象的某個(gè)屬性添加注冊(cè)觀察萎坷,當(dāng)該屬性的值發(fā)送變化時(shí)凹联,會(huì)觸發(fā)觀察者對(duì)象實(shí)現(xiàn)的KVO接口方法,自動(dòng)通知觀察者哆档。簡(jiǎn)單來(lái)說(shuō)KVO就是通過(guò)監(jiān)聽(tīng)key
來(lái)獲取所對(duì)應(yīng)的的value
的變化蔽挠,從而達(dá)到對(duì)象狀態(tài)變化的監(jiān)聽(tīng)。和KVC
一樣KVO
的定義也是對(duì)NSObject
的擴(kuò)展來(lái)實(shí)現(xiàn)的瓜浸,Objective-C中有個(gè)顯式的NSKeyValueObserving
類別名澳淑,所以對(duì)于所有派生于NSObject的類的對(duì)象,都能使用KVO插佛。
2杠巡、KVO的基礎(chǔ)使用
2.1、注冊(cè)觀察者
根據(jù)KVO的定義朗涩,KVO是對(duì)對(duì)象的屬性狀態(tài)變化的監(jiān)聽(tīng)忽孽,那么首先要對(duì)該對(duì)象(被觀察者)進(jìn)行注冊(cè)觀察者。
注冊(cè)觀察者的方法如下:
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
這個(gè)方法中的四個(gè)參數(shù)解釋如下:
observer
:注冊(cè)KVO通知的對(duì)象谢床,也就是觀察者兄一。觀察者必須實(shí)現(xiàn)
observeValueForKeyPath:ofObject:change:context:
。keyPath
:觀察者的屬性的keypath
识腿,相對(duì)于接受者出革,值不能是nil
。options
:KVO的一些屬性配置渡讼;有四個(gè)選項(xiàng)骂束。context
: 上下文,這個(gè)會(huì)傳遞到訂閱著的函數(shù)中成箫,用來(lái)區(qū)分消息展箱,所以應(yīng)當(dāng)是不同的。
其實(shí)前兩個(gè)參數(shù)比較好理解蹬昌,需要特別說(shuō)明的是后面兩個(gè)參數(shù)混驰。
2.1.1、options參數(shù)
options
參數(shù)是NSKeyValueObservingOptions
類型皂贩,是一個(gè)枚舉類型栖榨,其定義如下:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial = 0x04,
NSKeyValueObservingOptionPrior = 0x08
};
這個(gè)四個(gè)枚舉變量的含義如下:
NSKeyValueObservingOptionNew
: 表明變化的change字典應(yīng)該提供新的屬性值。NSKeyValueObservingOptionOld
: 表明變化的字典應(yīng)該包含舊的屬性值明刷。NSKeyValueObservingOptionInitial
:是否應(yīng)在觀察者注冊(cè)方法返回之前立即將通知發(fā)送給觀察者婴栽。如果NSKeyValueObservingOptionNew
也被指定,則通知中的change字典將始終包含一個(gè)NSKeyValueChangeNewKey
辈末,但絕不會(huì)包含一個(gè)NSKeyValueChangeOldKey
愚争。(在初始通知中映皆,觀察到的屬性的當(dāng)前值可能是舊的,但是對(duì)于觀察者來(lái)說(shuō)是新的准脂。)劫扒。NSKeyValueObservingOptionPrior
:是否應(yīng)該在每次更改之前和之后向觀察者發(fā)送單獨(dú)的通知,而不是在更改之后發(fā)送單個(gè)通知狸膏。在更改之前發(fā)送的通知中的change字典中會(huì)包含有一個(gè)notificationIsPrior
項(xiàng)沟饥,用以區(qū)分是在更改前發(fā)送的通知,但不會(huì)包含有NSKeyValueChangeNewKey
湾戳,即使是NSKeyValueObservingOptionNew
被指定贤旷。
看如下例子:
在注冊(cè)觀察者的時(shí)候options的參數(shù)傳入的是NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
,這個(gè)時(shí)候在進(jìn)入到頁(yè)面的時(shí)砾脑,打印的結(jié)果中有一個(gè)new
項(xiàng)幼驶,其實(shí)這個(gè)時(shí)候的name的只是舊的屬性值。在點(diǎn)擊屏幕觸發(fā)修改name屬性后打印的結(jié)果中的new的結(jié)果是新的屬性值韧衣。
正如上圖所示盅藻,如果options參數(shù)傳入的是
NSKeyValueObservingOptionPrior
,則會(huì)在更改前后各發(fā)一次通知畅铭,不管是否有傳入NSKeyValueObservingOptionNew
氏淑,在更改前的通知中的change字典中都不會(huì)包含有NSKeyValueChangeNewKey
項(xiàng)。
2.1.1硕噩、context參數(shù)
context
指針在addObserver:forKeyPath:options:context: message
中包含任意的數(shù)據(jù)假残,這些數(shù)據(jù)將在相應(yīng)的變更通知中被傳遞回觀察者。您可以指定NULL
并完全依賴于keyPath
來(lái)確定更改通知的來(lái)源炉擅,但是這種方法可能會(huì)對(duì)一個(gè)對(duì)象造成問(wèn)題辉懒,因?yàn)樵搶?duì)象的超類由于不同的原因也在觀察相同的keyPath
。一種更安全谍失、更可擴(kuò)展的方法是使用context
來(lái)確保接收到的通知是針對(duì)觀察者的眶俩,而不是超類。類中唯一命名的靜態(tài)變量的地址是一個(gè)很好的context快鱼。在超類或子類中以類似方式選擇的context不太可能重疊仿便。可以為整個(gè)類選擇一個(gè)context,并依賴于通知消息中的keyPath來(lái)確定更改了什么攒巍。或者荒勇,也可以為每個(gè)觀察到的keyPath創(chuàng)建不同的上下文柒莉,從而完全繞過(guò)字符串比較的需要,從而提高通知解析的效率沽翔。
正如下面所示的那樣是為Person類和Student類的name屬性創(chuàng)建的context兢孝。這樣只需要在接收通知的時(shí)候判斷context邊可以分辨出是哪個(gè)對(duì)象的屬性發(fā)生了改變窿凤。
static void * PersonNameContext = &PersonNameContext;
static void * StudentNameContext = &StudentNameContext;
2.2、觀察者接收消息
觀察者接收消息的方法如下:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
在這個(gè)方法的里面除了change外的其他幾個(gè)參數(shù)都是在添加注冊(cè)觀察者的時(shí)候傳入的參數(shù)原樣帶回跨蟹,在這里可以通過(guò)keyPath來(lái)匹配確認(rèn)改變的屬性雳殊,也可以通過(guò)object和keyPath相結(jié)合的方式來(lái)區(qū)分確認(rèn)是哪個(gè)對(duì)象的那個(gè)屬性發(fā)生了修改,但是這樣不免在代碼的合理性和優(yōu)雅上大打折扣了窗轩,所以最好的方式是通過(guò)context來(lái)區(qū)分夯秃。change這個(gè)字典保存了變更的信息,其內(nèi)容和你在添加注冊(cè)的時(shí)候傳入的options參數(shù)有關(guān)痢艺。
2.3仓洼、手動(dòng)觀察
按照上面章節(jié)所講可以實(shí)現(xiàn)對(duì)對(duì)象屬性的監(jiān)聽(tīng),那是因?yàn)閷傩灾档淖兓上到y(tǒng)控制的堤舒,開(kāi)發(fā)者只需要告訴系統(tǒng)監(jiān)聽(tīng)什么屬性便可以了色建,但是在實(shí)際的開(kāi)發(fā)中我們有可能屬性的值的變化并不需要受系統(tǒng)的支配。實(shí)際上除了系統(tǒng)自動(dòng)監(jiān)聽(tīng)屬性值的變化外舌缤,還有一種方式便是可以由開(kāi)發(fā)者支配屬性的值變化后是否發(fā)送通知箕戳。只需要修改類方法 automaticallyNotifiesObserversForKey:
的返回值,如果返回 YES
就是自動(dòng)国撵,返回 NO
就是手動(dòng)陵吸。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
一旦automaticallyNotifiesObserversForKey
方法的返回的NO,系統(tǒng)就不在自動(dòng)監(jiān)控屬性的值變化卸留,如果想要還能監(jiān)控到屬性的值變化走越,那么還需要調(diào)用兩個(gè)方法:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
只需要在屬性值修改的前后分別調(diào)用這兩個(gè)方法便可。
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
需要注意的是對(duì)于一個(gè)容器類的屬性耻瑟,不僅必須指定已更改的key
旨指,還必須指定更改的類型和所涉及對(duì)象的索引。 更改的類型是 NSKeyValueChange
喳整,它指定 NSKeyValueChangeInsertion谆构,NSKeyValueChangeRemoval 或 NSKeyValueChangeReplacement
,受影響的對(duì)象的索引作為 NSIndexSet
對(duì)象傳遞:
- (void)removeObjectFromMArrayAtIndex:(NSUInteger)index {
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"mArray"];
[self.mArray removeObjectAtIndex:index];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"mArray"];
}
2.4框都、依賴鍵
有時(shí)候一個(gè)屬性的值依賴于另一對(duì)象中的一個(gè)或多個(gè)屬性搬素,如果這些屬性中任一屬性的值發(fā)生變更,被依賴的屬性值也應(yīng)當(dāng)為其變更進(jìn)行標(biāo)記魏保。因此熬尺,object 引入了依賴鍵。
2.4.1谓罗、一對(duì)一關(guān)系
一對(duì)一的這種依賴關(guān)系實(shí)現(xiàn)自動(dòng)的KVO有兩種方式粱哼,一種是重寫(xiě)keyPathsForValuesAffectingValueForKey
方法,一種是實(shí)現(xiàn)一個(gè)合適的方法檩咱。
比如說(shuō)fullName
這個(gè)屬性依賴于firstName
和lastName
揭措,只要是兩者中任一修改都會(huì)影響到fullName胯舷,那么就可以在每次監(jiān)聽(tīng)到兩者中任一變化后對(duì)fullName進(jìn)行值修改便可以達(dá)到目的。
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
但是這樣的實(shí)現(xiàn)不免有些麻煩绊含,重寫(xiě)keyPathsForValuesAffectingValueForKey
方法桑嘶,使得fullNam的監(jiān)聽(tīng)和firstName、lastName相關(guān)聯(lián)躬充,這樣的方式會(huì)更加簡(jiǎn)單逃顶。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
如上代碼所示,通過(guò)實(shí)現(xiàn)類方法 keyPathsForValuesAffectingValueForKey
來(lái)返回一個(gè)集合麻裳,這樣就實(shí)現(xiàn)了fullName和firstName口蝠、lastName的聯(lián)動(dòng)。
實(shí)際上還有一個(gè)便利的方法津坑,就是 keyPathsForValuesAffecting<Key>
妙蔗,Key
是屬性的名稱(需要首字母大寫(xiě))。這個(gè)方法的效果和 keyPathsForValuesAffectingValueForKey
是一樣的疆瑰,但針對(duì)的某個(gè)具體屬性眉反。
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
2.4.2、一對(duì)多關(guān)系
keyPathsForValuesAffectingValueForKey
方法不支持包含一對(duì)多關(guān)系的Key Path
穆役。例如寸五,假設(shè)你有一個(gè)Department對(duì)象,該對(duì)象與Employee 有一對(duì)多關(guān)系(即 employees 屬性)耿币,而 Employee 具有salary 屬性梳杏。 如果需要在Department 對(duì)象上增加totalSalary 屬性,而該屬性取決于關(guān)系中所有Employees的薪水淹接。例如十性,您不能使用keyPathsForValuesAffectingTotalSalary 和返回employees.salary 作為鍵來(lái)執(zhí)行此操作。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
} else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (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;
}
2.5塑悼、取消注冊(cè)
在合適的地方取消注冊(cè)是一個(gè)必要的過(guò)程劲适,否則會(huì)造成不可預(yù)估的錯(cuò)誤,建議是取消注冊(cè)和添加注冊(cè)是一對(duì)一關(guān)系厢蒜。取消注冊(cè)的兩個(gè)方法如下:
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
2.6霞势、KVO和線程
一個(gè)需要注意的地方是,KVO 行為是同步的斑鸦,并且與所觀察的值發(fā)生變化的同樣的線程上愕贡。沒(méi)有隊(duì)列或者 Run-loop 的處理。所以巷屿,當(dāng)我們?cè)噲D從其他線程改變屬性值的時(shí)候我們應(yīng)當(dāng)十分小心固以,除非能確定所有的觀察者都用線程安全的方法處理 KVO 通知。通常來(lái)說(shuō)攒庵,我們不推薦把 KVO 和多線程混起來(lái)嘴纺。如果我們要用多個(gè)隊(duì)列和線程,我們不應(yīng)該在它們互相之間用 KVO浓冒。
3栽渴、KVO的實(shí)現(xiàn)原理
在前面的章節(jié)中介紹了KVO的基本是否,但是對(duì)于KVO的實(shí)現(xiàn)原理還沒(méi)有一個(gè)清晰的概念稳懒,好在KVO的官方文檔有對(duì)于KVO的實(shí)現(xiàn)原理有一個(gè)明確的說(shuō)明闲擦。
Automatic key-value observing is implemented using a technique called isa-swizzling.
【譯】使用isa-swizzling
技術(shù)實(shí)現(xiàn)了鍵值的自動(dòng)觀察。
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.
【譯】顧名思義场梆,isa
指針指向?qū)ο蟮念?這個(gè)類維護(hù)了一個(gè)調(diào)度表墅冷。這個(gè)調(diào)度表本質(zhì)上包含指向類實(shí)現(xiàn)的方法和其他數(shù)據(jù)的指針。
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.
【譯】當(dāng)一個(gè)觀察者為一個(gè)對(duì)象的屬性注冊(cè)時(shí)或油,被觀察對(duì)象的isa
指針被修改寞忿,指向一個(gè)中間類
而不是真正的類。因此顶岸,isa指針的值不一定反映實(shí)例的實(shí)際類腔彰。
根據(jù)這段話可以得知一個(gè)信息,那就是KVO實(shí)際上是生成了一個(gè)中間類辖佣,并且將被觀察對(duì)象的isa指向這個(gè)中間類霹抛,但是這個(gè)中間類是什么?這個(gè)中間類和真是的類之間是什么關(guān)系卷谈?KVO是怎樣觀察被觀察對(duì)象的屬性變化的杯拐?這個(gè)中間類會(huì)不會(huì)隨著取消注冊(cè)而消亡?帶著這些疑問(wèn)世蔗,我們進(jìn)入到下一步的探索端逼。
3.1、所謂中間類
在上面有提到被觀察對(duì)象的isa
是指向這個(gè)中間類的凸郑,那么我們便可以通過(guò)LLDB指令的方式來(lái)查探這個(gè)中間類裳食。
如上圖所示在為注冊(cè)觀察者之前person對(duì)象的isa指向的Person類,在注冊(cè)觀察者之后person對(duì)象的isa指向的是一個(gè)NSKVONotifying_Person
類芙沥,那么我們便可以確定這個(gè)生成的中間類是NSKVONotifying_XX
的類诲祸。
在這里需要注意的是如果在外部創(chuàng)建了一個(gè)
NSKVONotifying_XX
的類,KVO將無(wú)法正常工作而昨。
3.2救氯、NSKVONotifying_Person和Person的關(guān)系
我們已經(jīng)知道了生成的中間類是NSKVONotifying_Person
,而且這個(gè)和Person類之間一定存在這某種關(guān)系歌憨,那么我們需要知道NSKVONotifying_Person
類的繼承鏈關(guān)系着憨。
如上圖所示,我們嘗試著去獲取NSKVONotifying_Person
類的父類务嫡,發(fā)現(xiàn)NSKVONotifying_Person類的父類是Person類甲抖,也就是說(shuō)NSKVONotifying_Person從Person繼承而來(lái)漆改。
3.3、如何觀察屬性變化
KVO觀察的是屬性值的變化准谚,那么對(duì)于一個(gè)屬性來(lái)說(shuō)其值的修改其實(shí)是調(diào)用的setter
方法或者通過(guò)KVC的方式挫剑。如果對(duì)于屬性和實(shí)例變量同時(shí)監(jiān)聽(tīng)其變化會(huì)怎么樣呢?
如上圖所示同時(shí)對(duì)實(shí)例變量nickName和屬性name進(jìn)行了注冊(cè)觀察者柱衔,發(fā)現(xiàn)只有屬性name能夠接受到變化的通知樊破,而實(shí)例變量監(jiān)控不到變化。但是如果使用KVC的方式來(lái)訪問(wèn)實(shí)例變量便可以監(jiān)控到其值的變化唆铐。
那么屬性而言屬性值變化的監(jiān)聽(tīng)是通過(guò)其setter方法來(lái)實(shí)現(xiàn)的哲戚。如果用戶注冊(cè)了對(duì)某個(gè)對(duì)象的某一個(gè)屬性的觀察,那么此派生類會(huì)重寫(xiě)這個(gè)方法艾岂,并在其中添加進(jìn)行通知的代碼顺少。Objective-C 在發(fā)送消息的時(shí)候,會(huì)通過(guò) isa 指針找到當(dāng)前對(duì)象所屬的類對(duì)象澳盐。而類對(duì)象中保存著當(dāng)前對(duì)象的實(shí)例方法祈纯,因此在向此對(duì)象發(fā)送消息時(shí)候,實(shí)際上是發(fā)送到了派生類(NSKVONotifying_Person)對(duì)象的方法叼耙。由于編譯器對(duì)派生類的方法進(jìn)行了復(fù)寫(xiě)腕窥,并添加了通知代碼,因此會(huì)向注冊(cè)的對(duì)象發(fā)送通知筛婉。注意派生類只重寫(xiě)注冊(cè)了觀察者的屬性方法簇爆。
NSKVONotifying_Person類除了重寫(xiě)Person類屬性的setter方法,還會(huì)重寫(xiě)class
爽撒、dealloc
入蛆、_isKVOA
等方法。之所以要重寫(xiě)class
方法其目的就是為了隱藏NSKVONotifying_Person
這個(gè)類硕勿。而重寫(xiě)setter方法是為了在其中調(diào)用- (void)willChangeValueForKey:(NSString *)key;
方法和- (void)didChangeValueForKey:(NSString *)key;
方法哨毁,然后再didChangeValueForKey
中調(diào)用observeValueForKeyPath
方法用以通知外界屬性值發(fā)生了變化。
3.3源武、NSKVONotifying_Person類的消亡
NSKVONotifying_Person類是在注冊(cè)觀察者后生成的扼褪,那么會(huì)不會(huì)在取消注冊(cè)后會(huì)消亡呢?在取消注冊(cè)之后代用打印類的方法名稱集的方法粱栖,如果有打印結(jié)果顯示則說(shuō)明NSKVONotifying_Person類并不會(huì)隨著取消注冊(cè)而消亡话浇。
如上圖所示,在取消注冊(cè)觀察者之后打印出了NSKVONotifying_Person類的所有方法闹究,說(shuō)明NSKVONotifying_Person這個(gè)類在取消注冊(cè)觀察者之后依然存在幔崖。在取消注冊(cè)觀察者后person對(duì)象的isa又指向了Person類。
4、 KVO的優(yōu)缺點(diǎn)
4.1赏寇、KVO的優(yōu)點(diǎn)
- KVO提供了一種簡(jiǎn)單的方法實(shí)現(xiàn)兩個(gè)對(duì)象間的同步吉嫩。例如:model和view之間同步;
- 能夠?qū)Ψ俏覀儎?chuàng)建的對(duì)象嗅定,即內(nèi)部對(duì)象的狀態(tài)改變作出響應(yīng)率挣,而且不需要改變內(nèi)部對(duì)象(SKD對(duì)象)的實(shí)現(xiàn);
- 能夠提供觀察的屬性的最新值以及先前值露戒;
- 用key paths來(lái)觀察屬性,因此也可以觀察嵌套對(duì)象捶箱;
- 完成了對(duì)觀察對(duì)象的抽象智什,因?yàn)椴恍枰~外的代碼來(lái)允許觀察值能夠被觀察
4.2、KVO的缺點(diǎn)
- 我們觀察的屬性必須使用strings來(lái)定義丁屎。因此在編譯器不會(huì)出現(xiàn)警告以及檢查荠锭;
- 對(duì)屬性重構(gòu)將導(dǎo)致我們的觀察代碼不再可用;
- 觀察多個(gè)屬性時(shí)晨川,需要些復(fù)雜的if判斷條件語(yǔ)句证九;
- 當(dāng)釋放觀察者時(shí)不需要移除觀察者。
5共虑、自定義KVO
在上邊的章節(jié)中詳細(xì)分析了KVO的使用和實(shí)現(xiàn)原理愧怜,知道了KVO的原理其實(shí)是生成了一個(gè)
NSKVONotifying_XX
(XX表示被觀察對(duì)象的所屬類)的中間類
,這個(gè)中間類繼承自被觀察對(duì)象的所屬類妈拌,并且重寫(xiě)了這個(gè)類的屬性的setter
方法和class
方法拥坛,同時(shí),觀察者對(duì)象的isa
指針的指向不在是指向?qū)ο蟮乃鶎兕惓痉郑侵赶蜻@個(gè)中間類猜惋。KVO觀察屬性的變化其實(shí)是觀察屬性的setter方法的調(diào)用,在中間了重寫(xiě)父類的setter方法中培愁,會(huì)調(diào)用willChangeValueForKey
方法和didChangeValueForKey
方法著摔,而在didChangeValueForKey方法內(nèi)部會(huì)調(diào)用observeValueForKeyPath
方法將屬性的變化通知給觀察者。當(dāng)取消注冊(cè)觀察者的時(shí)候定续,被觀察對(duì)象的isa指針會(huì)重寫(xiě)指向所屬類谍咆。
5.1、基本思路
根據(jù)KVO的實(shí)現(xiàn)原理設(shè)計(jì)自定義KVO的基本思路如下:
- 首先檢查被觀察對(duì)象的屬性的setter方法是有實(shí)現(xiàn)香罐;
- 動(dòng)態(tài)生一個(gè)中間類繼承自被觀察對(duì)象的所屬類卧波;
- 為中間類添加setter方法、class方法庇茫、dealloc方法港粱;
- 修改被觀察對(duì)象的isa指向中間類;
- 在中間類的setter方法里面進(jìn)行消息發(fā)送給父類,通過(guò)block的方式將屬性的新值和舊值傳回查坪;
- 在dealloc方法里面進(jìn)行isa重寫(xiě)指回寸宏。
5.2、注冊(cè)觀察者
按照上面的思路偿曙,首先是要對(duì)被觀察對(duì)象的屬性的setter
方法進(jìn)行驗(yàn)證氮凝。驗(yàn)證的關(guān)鍵點(diǎn)就是對(duì)于參入的keyPath
進(jìn)行setter的方法的拼接,因?yàn)檫@里的keyPath實(shí)際上就是被觀察對(duì)象的被觀察屬性望忆。
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath {
Class superClass = object_getClass(self);
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"沒(méi)有當(dāng)前%@的setter罩阵,請(qǐng)檢查keyPath參數(shù)的正確性", keyPath] userInfo:nil];
}
}
然后需要?jiǎng)討B(tài)的創(chuàng)建一個(gè)中間類,繼承自被觀察對(duì)象的所屬類启摄,并且要重寫(xiě)setter和class方法稿壁。
static NSString *const kDSKVOPrefix = @"DSKVONotifying_";
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@", kDSKVOPrefix, oldClassName];
Class newClass = NSClassFromString(newClassName);
// 防止重復(fù)創(chuàng)建生成新類
if (newClass) return newClass;
// 申請(qǐng)類
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注冊(cè)類
objc_registerClassPair(newClass);
// 添加class : class的指向是父類
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)ds_class, classTypes);
// 添加setter
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)ds_setter, setterTypes);
// 2.3.3 : 添加dealloc
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)ds_dealloc, deallocTypes);
return newClass;
}
Class ds_class(id self, SEL _cmd)
{
return class_getSuperclass(object_getClass(self));
}
然后需要修改被觀察對(duì)象的isa
指向中間類,并且需要將KVO的信息進(jìn)行對(duì)象化保存歉备。完整的注冊(cè)觀察者的方法代碼如下:
static NSString *const kDSKVOAssiociateKey = @"kDSKVO_AssiociateKey";
- (void)ds_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(DSKVOBlock)block {
// 1: 驗(yàn)證是否存在setter方法 : 不讓實(shí)例進(jìn)來(lái)
[self judgeSetterMethodFromKeyPath:keyPath];
// 2: 動(dòng)態(tài)生成子類
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3: 修改isa的指向
object_setClass(self, newClass);
// 4: 保存信息
DSKVOInfo *info = [[DSKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kDSKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kDSKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
}
在這里定義了一個(gè)類來(lái)對(duì)象化KVO的信息傅是,并且用關(guān)聯(lián)對(duì)象的形式進(jìn)行信息的保存和取出。
typedef void(^DSKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
@interface DSKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;//觀察者蕾羊,這里用weak修飾喧笔,避免出現(xiàn)循環(huán)引用
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) DSKVOBlock handleBlock;
@end
@implementation DSKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(DSKVOBlock)block {
if (self = [super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
5.3、通知觀察者
根據(jù)KVO的原理龟再,觀察者觀察到被觀察者對(duì)象狀態(tài)的變化其實(shí)是觀察的屬性的setter方法书闸,那么我們就可以定義自己的setter方法,只需要發(fā)送出狀態(tài)變化后的值的消息就可以了利凑。
/// 自定義setter方法梗劫,屬于中間類,父類發(fā)送消息
/// @param _cmd 方法編號(hào)截碴,被觀察屬性的setter方法
/// @param newValue 屬性的新的值
static void ds_setter(id self, SEL _cmd, id newValue)
{
NSLog(@"來(lái)了:%@", newValue);
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
// 消息轉(zhuǎn)發(fā) : 轉(zhuǎn)發(fā)給父類
// 改變父類的值 --- 可以強(qiáng)制類型轉(zhuǎn)換
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
//objc_msgSendSuper(&superStruct,_cmd,newValue);
void (*ds_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
ds_msgSendSuper(&superStruct, _cmd, newValue);
//信息數(shù)據(jù)回調(diào)
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kDSKVOAssiociateKey));
for (DSKVOInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}
在這里之所以要想父類的setter方法發(fā)送消息是為了修改調(diào)用父類的setter方法修改屬性值梳侨。
5.4、自動(dòng)移除觀察者
移除觀察者其實(shí)就是講被觀察者對(duì)象的isa指針指回去日丹。
static void ds_dealloc(id self, SEL _cmd)
{
NSLog(@"ds_dealloc");
Class superClass = [self class];
object_setClass(self, superClass);
}
這里的自定義KVO代碼比較簡(jiǎn)單走哺,其目的只是為了更好的理解KVO的實(shí)現(xiàn)原理。實(shí)際上這里的代碼還存在很多的問(wèn)題哲虾,比如說(shuō)Options參數(shù)處理丙躏,context參數(shù)處理,觀察嵌套對(duì)象的處理束凑,多線程問(wèn)題等等一系列的問(wèn)題晒旅,所以這里的代碼不具備代碼設(shè)計(jì)的完整性。如果想要更加優(yōu)雅的使用KVO汪诉,建議大家去閱讀KVOController的源碼废恋,如果對(duì)于KVO的實(shí)現(xiàn)原理沒(méi)有那么清晰的認(rèn)識(shí)谈秫,可以去GNU下載閱讀KVO的相關(guān)代碼。
6鱼鼓、總結(jié)
- KVO調(diào)用
addObserver:
方法注冊(cè)觀察者拟烫,observer是觀察者對(duì)象,keyPath是被觀察者的屬性名稱迄本,不能為nil硕淑,options參數(shù)的傳入關(guān)系到接收通知的change字典的值,context上下問(wèn)為區(qū)分對(duì)象屬性變化提供有效途徑嘉赎。 - 觀察者實(shí)現(xiàn)
observeValueForKeyPath
方法接收屬性變化的通知置媳。 - 調(diào)用
removeObserver
方法實(shí)現(xiàn)取消注冊(cè)觀察者,這是一個(gè)必要的過(guò)程公条。 - KVO的原理在于生成了一個(gè)繼承于被觀察者對(duì)象的類的中間類半开,這個(gè)中間類重寫(xiě)了父類的屬性的
setter
方法,并且修改了被觀察者對(duì)象的isa
指針指向赃份,重寫(xiě)的setter方法里面調(diào)用了willChangeValueForKey
和didChangeValueForKey
方法,而在didChangeValueForKey方法內(nèi)部會(huì)調(diào)用observeValueForKeyPath
方法奢米,從而達(dá)到了屬性修改后通知觀察者的目的抓韩。 - 在取消注冊(cè)觀察者后生成的中間類并不會(huì)消亡,并且被觀察者對(duì)象的isa指針會(huì)重新指向原來(lái)的類鬓长。