(轉(zhuǎn))如何自己動手實現(xiàn) KVO

轉(zhuǎn)自:http://tech.glowing.com/cn/implement-kvo/
本文是 Objective-C Runtime 系列文章的第三篇。如果你對 Objective-C Runtime 還不是很了解削咆,可以先去看看前兩篇文章:
Objective-C Runtime
Method Swizzling 和 AOP 實踐

本篇會探究 KVO (Key-Value Observing) 實現(xiàn)機制恋博,并去實踐一番 - 利用 Runtime 自己動手去實現(xiàn) KVO 雹仿。
KVO (Key-Value Observing)
KVO 是 Objective-C 對觀察者模式(Observer Pattern)的實現(xiàn)重付。也是 Cocoa Binding 的基礎(chǔ)。當(dāng)被觀察對象的某個屬性發(fā)生更改時障贸,觀察者對象會獲得通知错森。
有意思的是,你不需要給被觀察的對象添加任何額外代碼篮洁,就能使用 KVO 问词。這是怎么做到的?
KVO 實現(xiàn)機制
KVO 的實現(xiàn)也依賴于 Objective-C 強大的 Runtime 嘀粱。Apple 的文檔有簡單提到過 KVO 的實現(xiàn)
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
指針會指向一個中間類,而不是原來真正的類锋叨÷⒎郑看來,Apple 并不希望過多暴露 KVO 的實現(xiàn)細節(jié)娃磺。不過薄湿,要是你用 runtime 提供的方法去深入挖掘,所有被掩蓋的細節(jié)都會原形畢露。Mike Ash 早在 2009 年就做了這么個探究豺瘤。
簡單概述下 KVO 的實現(xiàn):
當(dāng)你觀察一個對象時吆倦,一個新的類會動態(tài)被創(chuàng)建。這個類繼承自該對象的原本的類坐求,并重寫了被觀察屬性的 setter
方法蚕泽。自然,重寫的 setter
方法會負責(zé)在調(diào)用原 setter
方法之前和之后须妻,通知所有觀察對象值的更改。最后把這個對象的 isa
指針 ( isa
指針告訴 Runtime 系統(tǒng)這個對象的類是什么 ) 指向這個新創(chuàng)建的子類泛领,對象就神奇的變成了新創(chuàng)建的子類的實例荒吏。
原來,這個中間類渊鞋,繼承自原本的那個類绰更。不僅如此,Apple 還重寫了-class
方法锡宋,企圖欺騙我們這個類沒有變儡湾,就是原本那個類。更具體的信息员辩,去跑一下 Mike Ash 的那篇文章里的代碼就能明白盒粮,這里就不再重復(fù)鸵鸥。
KVO 缺陷
KVO 很強大奠滑,沒錯。知道它內(nèi)部實現(xiàn)妒穴,或許能幫助更好地使用它宋税,或在它出錯時更方便調(diào)試。但官方實現(xiàn)的 KVO 提供的 API 實在不怎么樣讼油。
比如杰赛,你只能通過重寫 -observeValueForKeyPath:ofObject:change:context:
方法來獲得通知。想要提供自定義的 selector
矮台,不行乏屯;想要傳一個 block
,門都沒有瘦赫。而且你還要處理父類的情況 - 父類同樣監(jiān)聽同一個對象的同一個屬性辰晕。但有時候,你不知道父類是不是對這個消息有興趣确虱。雖然 context
這個參數(shù)就是干這個的含友,也可以解決這個問題 - 在 -addObserver:forKeyPath:options:context:
傳進去一個父類不知道的 context
。但總覺得框在這個 API 的設(shè)計下,代碼寫的很別扭窘问。至少至少辆童,也應(yīng)該支持 block
吧。
有不少人都覺得官方 KVO 不好使的惠赫。Mike Ash 的 Key-Value Observing Done Right把鉴,以及獲得不少分享討論的 KVO Considered Harmful 都把 KVO 拿出來吊打了一番。所以在實際開發(fā)中 KVO 使用的情景并不多汉形,更多時候還是用 Delegate 或 NotificationCenter纸镊。
自己實現(xiàn) KVO
如果沒找到理想的,就自己動手做一個概疆。既然我們對官方的 API 不太滿意逗威,又知道如何去實現(xiàn)一個 KVO,那就嘗試自己動手寫一個簡易的 KVO 玩玩岔冀。
首先凯旭,我們創(chuàng)建 NSObject 的 Category,并在頭文件中添加兩個 API:
typedef void(^PGObservingBlock)(id observedObject, NSString *observedKey, id oldValue, id newValue);@interface NSObject (KVO)- (void)PG_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(PGObservingBlock)block;- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;@end

接下來使套,實現(xiàn) PG_addObserver:forKey:withBlock:
方法罐呼。邏輯并不復(fù)雜:
檢查對象的類有沒有相應(yīng)的 setter 方法。如果沒有拋出異常侦高;
檢查對象 isa
指向的類是不是一個 KVO 類嫉柴。如果不是,新建一個繼承原來類的子類奉呛,并把 isa
指向這個新建的子類计螺;
檢查對象的 KVO 類重寫過沒有這個 setter 方法。如果沒有瞧壮,添加重寫的 setter 方法登馒;
添加這個觀察者

  • (void)PG_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(PGObservingBlock)block{ // Step 1: Throw exception if its class or superclasses doesn't implement the setter SEL setterSelector = NSSelectorFromString(setterForGetter(key)); Method setterMethod = class_getInstanceMethod([self class], setterSelector); if (!setterMethod) { // throw invalid argument exception } Class clazz = object_getClass(self); NSString *clazzName = NSStringFromClass(clazz); // Step 2: Make KVO class if this is first time adding observer and // its class is not an KVO class yet if (![clazzName hasPrefix:kPGKVOClassPrefix]) { clazz = [self makeKvoClassWithOriginalClassName:clazzName]; object_setClass(self, clazz); } // Step 3: Add our kvo setter method if its class (not superclasses) // hasn't implemented the setter if (![self hasSelector:setterSelector]) { const char *types = method_getTypeEncoding(setterMethod); class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types); } // Step 4: Add this observation info to saved observation objects PGObservationInfo *info = [[PGObservationInfo alloc] initWithObserver:observer Key:key block:block]; NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers)); if (!observers) { observers = [NSMutableArray array]; objc_setAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [observers addObject:info];}

再來一步一步細看。
第一步里咆槽,先通過 setterForGetter()
方法獲得相應(yīng)的 setter
的名字(SEL)陈轿。也就是把 key
的首字母大寫,然后前面加上 set
后面加上 :
秦忿,這樣 key
就變成了 setKey:
麦射。然后再用 class_getInstanceMethod
去獲得 setKey:
的實現(xiàn)(Method)。如果沒有灯谣,自然要拋出異常潜秋。
第二步,我們先看類名有沒有我們定義的前綴酬屉。如果沒有半等,我們就去創(chuàng)建新的子類揍愁,并通過 object_setClass()
修改 isa
指針。

  • (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName{ NSString *kvoClazzName = [kPGKVOClassPrefix stringByAppendingString:originalClazzName]; Class clazz = NSClassFromString(kvoClazzName); if (clazz) { return clazz; } // class doesn't exist yet, make it Class originalClazz = object_getClass(self); Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0); // grab class method's signature so we can borrow it Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class)); const char *types = method_getTypeEncoding(clazzMethod); class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types); objc_registerClassPair(kvoClazz); return kvoClazz;}

動態(tài)創(chuàng)建新的類需要用 objc/runtime.h
中定義的 objc_allocateClassPair()
函數(shù)杀饵。傳一個父類莽囤,類名,然后額外的空間(通常為 0)切距,它返回給你一個類朽缎。然后就給這個類添加方法,也可以添加變量谜悟。這里话肖,我們只重寫了 class
方法。哈哈葡幸,跟 Apple 一樣最筒,這時候我們也企圖隱藏這個子類的存在。最后 objc_registerClassPair()
告訴 Runtime 這個類的存在蔚叨。
第三步床蜘,重寫 setter 方法。新的 setter 在調(diào)用原 setter 方法后蔑水,通知每個觀察者(調(diào)用之前傳入的 block ):
static void kvo_setter(id self, SEL _cmd, id newValue) { NSString *setterName = NSStringFromSelector(_cmd); NSString getterName = getterForSetter(setterName); if (!getterName) { // throw invalid argument exception } id oldValue = [self valueForKey:getterName]; struct objc_super superclazz = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)) }; // cast our pointer so the compiler won't complain void (objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper; // call super's setter, which is original class's setter method objc_msgSendSuperCasted(&superclazz, _cmd, newValue); // look up observers and call the blocks NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers)); for (PGObservationInfo *each in observers) { if ([each.key isEqualToString:getterName]) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ each.block(self, getterName, oldValue, newValue); }); } }}

細心的同學(xué)會發(fā)現(xiàn)我們對 objc_msgSendSuper
進行類型轉(zhuǎn)換邢锯。在 Xcode 6 里,新的 LLVM 會對 objc_msgSendSuper
以及 objc_msgSend
做嚴格的類型檢查搀别,如果不做類型轉(zhuǎn)換丹擎。Xcode 會抱怨有 too many arguments
的錯誤。(在 WWDC 2014 的視頻 What new in LLVM 中有提到過這個問題歇父。)
最后一步蒂培,把這個觀察的相關(guān)信息存在 associatedObject 里。觀察的相關(guān)信息(觀察者庶骄,被觀察的 key, 和傳入的 block )封裝在 PGObservationInfo
類里毁渗。
@interface PGObservationInfo : NSObject@property (nonatomic, weak) NSObject *observer;@property (nonatomic, copy) NSString *key;@property (nonatomic, copy) PGObservingBlock block;@end

就此践磅,一個基本的 KVO 就可以 work 了单刁。當(dāng)然,這只是一個一天多做出來的小東西府适,會有 bug羔飞,也有很多可以優(yōu)化完善的地方。但作為 demo 演示如何利用 Runtime 動態(tài)創(chuàng)建類檐春、如何實現(xiàn) KVO逻淌,足已。
完整的例子可以從這里下載:ImplementKVO
如果有任何問題或找到 bug疟暖,可以郵件我 peng@glowing.com 或者私信我的微博 @no_computer卡儒。
謝謝觀賞田柔。
Reference
KVO Implementation
Creating Classes at Runtime in Objective-C
Key-Value Observing Done Right
By your command
Associated Objects

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市骨望,隨后出現(xiàn)的幾起案子硬爆,更是在濱河造成了極大的恐慌,老刑警劉巖擎鸠,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缀磕,死亡現(xiàn)場離奇詭異,居然都是意外死亡劣光,警方通過查閱死者的電腦和手機袜蚕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绢涡,“玉大人牲剃,你說我怎么就攤上這事⌒劭桑” “怎么了颠黎?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長滞项。 經(jīng)常有香客問我狭归,道長,這世上最難降的妖魔是什么文判? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任过椎,我火速辦了婚禮,結(jié)果婚禮上戏仓,老公的妹妹穿的比我還像新娘疚宇。我一直安慰自己,他們只是感情好赏殃,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布敷待。 她就那樣靜靜地躺著,像睡著了一般仁热。 火紅的嫁衣襯著肌膚如雪榜揖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天抗蠢,我揣著相機與錄音举哟,去河邊找鬼。 笑死迅矛,一個胖子當(dāng)著我的面吹牛妨猩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播秽褒,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼壶硅,長吁一口氣:“原來是場噩夢啊……” “哼威兜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起庐椒,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤牡属,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扼睬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逮栅,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年窗宇,在試婚紗的時候發(fā)現(xiàn)自己被綠了措伐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡军俊,死狀恐怖侥加,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情粪躬,我是刑警寧澤担败,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站镰官,受9級特大地震影響提前,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泳唠,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一狈网、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧笨腥,春花似錦拓哺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至谆级,卻和暖如春烤礁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哨苛。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工鸽凶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留币砂,地道東北人建峭。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像决摧,于是被迫代替她去往敵國和親亿蒸。 傳聞我的和親對象是個殘疾皇子凑兰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

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

  • 來自 http://tech.glowing.com/cn/implement-kvo/ 本文是 Objectiv...
    MSG猿閱讀 355評論 0 0
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,715評論 0 9
  • 本文是 Objective-C Runtime 系列文章的第三篇边锁。如果你對 Objective-C Runtime...
    克魯?shù)吕?/span>閱讀 351評論 0 2
  • 上篇文章講到了什么是isa指針以及KVO的底層實現(xiàn)姑食,如果對KVO和isa指針不熟悉的需要先看看這篇文章。本篇文章主...
    lilei5閱讀 1,169評論 0 13
  • 提筆寫作時茅坛,常有這種情況:有些人寫著寫著就停筆音半,總說沒靈感,抓耳撓腮贡蓖、絞盡腦汁就是寫不出來曹鸠,而有些人卻能輕松在一天...
    光沐思維閱讀 312評論 0 0