(轉)如何自己動手實現 KVO

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

本篇會探究 KVO (Key-Value Observing) 實現機制,并去實踐一番 - 利用 Runtime 自己動手去實現 KVO 。
KVO (Key-Value Observing)
KVO 是 Objective-C 對觀察者模式(Observer Pattern)的實現。也是 Cocoa Binding 的基礎。當被觀察對象的某個屬性發生更改時,觀察者對象會獲得通知。
有意思的是,你不需要給被觀察的對象添加任何額外代碼,就能使用 KVO 。這是怎么做到的?
KVO 實現機制
KVO 的實現也依賴于 Objective-C 強大的 Runtime 。Apple 的文檔有簡單提到過 KVO 的實現
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 的實現細節。不過,要是你用 runtime 提供的方法去深入挖掘,所有被掩蓋的細節都會原形畢露。Mike Ash 早在 2009 年就做了這么個探究
簡單概述下 KVO 的實現:
當你觀察一個對象時,一個新的類會動態被創建。這個類繼承自該對象的原本的類,并重寫了被觀察屬性的 setter
方法。自然,重寫的 setter
方法會負責在調用原 setter
方法之前和之后,通知所有觀察對象值的更改。最后把這個對象的 isa
指針 ( isa
指針告訴 Runtime 系統這個對象的類是什么 ) 指向這個新創建的子類,對象就神奇的變成了新創建的子類的實例。
原來,這個中間類,繼承自原本的那個類。不僅如此,Apple 還重寫了-class
方法,企圖欺騙我們這個類沒有變,就是原本那個類。更具體的信息,去跑一下 Mike Ash 的那篇文章里的代碼就能明白,這里就不再重復。
KVO 缺陷
KVO 很強大,沒錯。知道它內部實現,或許能幫助更好地使用它,或在它出錯時更方便調試。但官方實現的 KVO 提供的 API 實在不怎么樣。
比如,你只能通過重寫 -observeValueForKeyPath:ofObject:change:context:
方法來獲得通知。想要提供自定義的 selector
,不行;想要傳一個 block
,門都沒有。而且你還要處理父類的情況 - 父類同樣監聽同一個對象的同一個屬性。但有時候,你不知道父類是不是對這個消息有興趣。雖然 context
這個參數就是干這個的,也可以解決這個問題 - 在 -addObserver:forKeyPath:options:context:
傳進去一個父類不知道的 context
。但總覺得框在這個 API 的設計下,代碼寫的很別扭。至少至少,也應該支持 block
吧。
有不少人都覺得官方 KVO 不好使的。Mike Ash 的 Key-Value Observing Done Right,以及獲得不少分享討論的 KVO Considered Harmful 都把 KVO 拿出來吊打了一番。所以在實際開發中 KVO 使用的情景并不多,更多時候還是用 Delegate 或 NotificationCenter。
自己實現 KVO
如果沒找到理想的,就自己動手做一個。既然我們對官方的 API 不太滿意,又知道如何去實現一個 KVO,那就嘗試自己動手寫一個簡易的 KVO 玩玩。
首先,我們創建 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

接下來,實現 PG_addObserver:forKey:withBlock:
方法。邏輯并不復雜:
檢查對象的類有沒有相應的 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()
方法獲得相應的 setter
的名字(SEL)。也就是把 key
的首字母大寫,然后前面加上 set
后面加上 :
,這樣 key
就變成了 setKey:
。然后再用 class_getInstanceMethod
去獲得 setKey:
的實現(Method)。如果沒有,自然要拋出異常。
第二步,我們先看類名有沒有我們定義的前綴。如果沒有,我們就去創建新的子類,并通過 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;}

動態創建新的類需要用 objc/runtime.h
中定義的 objc_allocateClassPair()
函數。傳一個父類,類名,然后額外的空間(通常為 0),它返回給你一個類。然后就給這個類添加方法,也可以添加變量。這里,我們只重寫了 class
方法。哈哈,跟 Apple 一樣,這時候我們也企圖隱藏這個子類的存在。最后 objc_registerClassPair()
告訴 Runtime 這個類的存在。
第三步,重寫 setter 方法。新的 setter 在調用原 setter 方法后,通知每個觀察者(調用之前傳入的 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); }); } }}

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

就此,一個基本的 KVO 就可以 work 了。當然,這只是一個一天多做出來的小東西,會有 bug,也有很多可以優化完善的地方。但作為 demo 演示如何利用 Runtime 動態創建類、如何實現 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 來自 http://tech.glowing.com/cn/implement-kvo/ 本文是 Objectiv...
    MSG猿閱讀 363評論 0 0
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,768評論 0 9
  • 本文是 Objective-C Runtime 系列文章的第三篇。如果你對 Objective-C Runtime...
    克魯德李閱讀 358評論 0 2
  • 上篇文章講到了什么是isa指針以及KVO的底層實現,如果對KVO和isa指針不熟悉的需要先看看這篇文章。本篇文章主...
    lilei5閱讀 1,189評論 0 13
  • 提筆寫作時,常有這種情況:有些人寫著寫著就停筆,總說沒靈感,抓耳撓腮、絞盡腦汁就是寫不出來,而有些人卻能輕松在一天...
    光沐思維閱讀 325評論 0 0