iOS中KVO的模擬實現(xiàn)

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ā)生了改變席吴。

上面只是大致了說了下底層的實現(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)用supersetter實現(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); 
}

首先利用Runtimeobjc_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.namedogname進行賦值時則不會調(diào)用dogset方法引润,是不會觸發(fā)KVO的巩趁,但是可以手動在person.dog.name的前后調(diào)用上面提到的willChangeValueForKeydidChangeValueForKey方法來觸發(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)知識的理解别凹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末草讶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子炉菲,更是在濱河造成了極大的恐慌堕战,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拍霜,死亡現(xiàn)場離奇詭異嘱丢,居然都是意外死亡,警方通過查閱死者的電腦和手機沉御,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門屿讽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吠裆,你說我怎么就攤上這事伐谈。” “怎么了试疙?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵诵棵,是天一觀的道長。 經(jīng)常有香客問我祝旷,道長履澳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任怀跛,我火速辦了婚禮距贷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吻谋。我一直安慰自己忠蝗,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布漓拾。 她就那樣靜靜地躺著阁最,像睡著了一般。 火紅的嫁衣襯著肌膚如雪骇两。 梳的紋絲不亂的頭發(fā)上速种,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機與錄音低千,去河邊找鬼配阵。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的闸餐。 我是一名探鬼主播饱亮,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼舍沙!你這毒婦竟也來了近上?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤拂铡,失蹤者是張志新(化名)和其女友劉穎壹无,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體感帅,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡斗锭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了失球。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岖是。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖实苞,靈堂內(nèi)的尸體忽然破棺而出豺撑,到底是詐尸還是另有隱情,我是刑警寧澤黔牵,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布聪轿,位于F島的核電站,受9級特大地震影響猾浦,放射性物質(zhì)發(fā)生泄漏陆错。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一金赦、第九天 我趴在偏房一處隱蔽的房頂上張望音瓷。 院中可真熱鬧,春花似錦夹抗、人聲如沸外莲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至磨确,卻和暖如春沽甥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乏奥。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工摆舟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓恨诱,卻偏偏與公主長得像媳瞪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子照宝,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容