iOS中KVO的底層實現(xiàn)原理
在開發(fā)中我們經(jīng)常使用addObserver:forKeyPath:options:context:
方法來觀察類的某個屬性的改變,然后在observeValueForKeyPath:ofObject:change:context:
方法中監(jiān)聽改變的回調(diào)透罢,其底層的實現(xiàn)原理大致如下:
- 利用
Runtime
動態(tài)的生成一個子類呕寝,類名是NSKVONotifying_
為前綴店乐。 - 蘋果為了隱藏KVO的實現(xiàn),重寫了子類
class
方法澜驮,返回的是父類的類對。 - 重寫了被監(jiān)聽屬性的
setter
方法,這是實現(xiàn)KVO
的關(guān)鍵释树,其實內(nèi)部調(diào)用了Foundation
框架下的_NSSet***ValueAndNotify
方法,看具體監(jiān)聽屬性的類型是什么擎淤,方法的調(diào)用名略有區(qū)別奢啥,這個方法的實現(xiàn)是KVO
的核心,其大致實現(xiàn)邏輯如下:- 調(diào)用
- (void)willChangeValueForKey:(NSString *)key
方法表明屬性即將發(fā)生改變嘴拢。 - 調(diào)用父類原來的
setter
方法的實現(xiàn)桩盲。 - 調(diào)用
- (void)didChangeValueForKey:(NSString *)key
方法表明屬性已改變,其中這個方法里面會調(diào)用observeValueForKeyPath: ofObject: change: context:
方法告知父類監(jiān)聽的屬性發(fā)生了改變席吴。
- 調(diào)用
上面只是大致了說了下底層的實現(xiàn)流程赌结,其實當(dāng)然還有一些其他的善后工作要做,這里我們不在深入研究抢腐,有興趣的可以利用查看源碼并用
Runtime
打印監(jiān)聽前后類的方法列表以及實現(xiàn)進行跟蹤驗證姑曙。
自定義KVO的實現(xiàn)
上面已經(jīng)簡要介紹了KVO
的實現(xiàn)原理,現(xiàn)在我們仿照上面的原理自己寫一個KVO
的實現(xiàn)迈倍,也大致分為以下幾個步驟:
- 動態(tài)生成一個子類伤靠。
- 重寫
setter
方法,在方法中,調(diào)用super
的setter
實現(xiàn)宴合,并通知觀察者焕梅。
首先定義一個NSObject(KVO)
的分類,然后仿照蘋果一樣定義一個- (void)wy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
方法來監(jiān)聽屬性卦洽,方法的具體實現(xiàn)如下:
- (void)wy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
//1.利用 runtime贞言,動態(tài)生成一個類
//1.1 創(chuàng)建self的子類
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [@"wykvo_" stringByAppendingString:oldClassName];
const char *newName = [newClassName UTF8String];
//創(chuàng)建一個類的class
Class MyClass = objc_allocateClassPair([self class], newName, 0);
//注冊類
objc_registerClassPair(MyClass);
//2.添加一個set方法
class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@");
//3.改變isa指針(這個好像不利于把方法寫成活的,采用方法交換可能更好)
object_setClass(self, MyClass);
//4.保存觀察者對象
objc_setAssociatedObject(self, @"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
首先利用Runtime
的objc_allocateClassPair
方法來動態(tài)生成一個子類阀蒂,并添加一個setName
的方法该窗,并改變isa
指針的指向為新生成的子類,同時利用關(guān)聯(lián)對象為分類添加一個objc
的屬性保存著觀察者對象以便后面通知觀察者屬性發(fā)生了改變蚤霞。
這里有幾個點需要注意酗失,由于這里只是簡單的模擬
name
屬性的改變,所以set屬性的方法名是寫死的昧绣,實際上應(yīng)該根據(jù)keypath
來動態(tài)確定规肴,這里不在深入;為了實現(xiàn)當(dāng)調(diào)用set
方法能調(diào)用到新類的set
方法上夜畴,采用了改變isa
的指針來實現(xiàn)拖刃,這樣在調(diào)用時會根據(jù)isa
的指向找到新類的實現(xiàn);同時由于分類中需要保存觀察者贪绘,由于分類是不能添加屬性的兑牡,這里采用了關(guān)聯(lián)對象來保存觀察者對象。
void setName(id self,SEL _cmd,NSString *newName){
//調(diào)用父類的set方法税灌,需要在build打開容許消息機制
//保存子類類型
id class = [self class];
//改變self的isa指針
object_setClass(self, class_getSuperclass(class));
((void (*)(id, SEL, NSString *))objc_msgSend)(self, @selector(setName:), newName);
//拿到通知觀察者
id objc = objc_getAssociatedObject(self, @"objc");
// 通知觀察者
((void (*)(id, SEL, id, NSString *, id, void *))objc_msgSend)(objc, @selector(observeValueForKeyPath:ofObject:change:context:),self,@"name",nil,nil);
//改回子類類型
object_setClass(self, class);
}
在setName
的實現(xiàn)中发绢,由于需要首先調(diào)用原來的set
實現(xiàn),所以再次將isa
指針指向原來的被觀察對象垄琐,同時利用objc_msgSend
消息發(fā)送機制調(diào)用set
方法,這樣會根據(jù)isa
指針首先找到觀察類的set
實現(xiàn)经柴,然后通過關(guān)聯(lián)對象拿到觀察者狸窘,利用objc_msgSend
調(diào)用相應(yīng)的方法完成通知。
注意點
在使用KVO
的過程中坯认,判斷某個屬性設(shè)置會不會觸發(fā)KVO
需要看是否調(diào)用了set
方法翻擒,比如如果直接對成員變量進行賦值則不會觸發(fā)KVO
機制,比如Person
類里面一個dog
對象屬性牛哺,dog
類有個name
屬性嗎陋气,當(dāng)我們監(jiān)聽dog
屬性時,如何用person.dog.name
對dog
的name
進行賦值時則不會調(diào)用dog
的set
方法引润,是不會觸發(fā)KVO
的巩趁,但是可以手動在person.dog.name
的前后調(diào)用上面提到的willChangeValueForKey
和didChangeValueForKey
方法來觸發(fā)KVO
機制。
總結(jié)
根據(jù)KVO
的底層的實現(xiàn)原理淳附,利用Runtime
的消息機制议慰,isa
指針和關(guān)聯(lián)對象等相關(guān)底層知識模仿實現(xiàn)了KVO
實現(xiàn)蠢古,這有助于進一步理解底層KVO
的實現(xiàn)原理,并加深對Runtime
的相關(guān)知識的理解别凹。