我的Github地址 : Jerry4me, 本文章的demo連結 : JRCustomKVODemo
前言
KVO(Key-Value Observing, 鍵值觀察), KVO的實現也依賴於runtime. 當你對一個物件進行觀察時, 系統會動態建立一個類繼承自原類, 然後重寫被觀察屬性的setter
方法. 然後重寫的setter
方法會負責在呼叫原setter
方法前後通知觀察者. KVO還會修改原物件的isa指標指向這個新類.
我們知道, 物件是通過isa指標去查詢自己是屬於哪個類, 並去所在類的方法列表中查詢方法的, 所以這個時候這個物件就自然地變成了新類的例項物件.
不僅如此, Apple還重寫了原類的- class
方法, 檢視欺騙我們, 這個類沒有變, 還是原來的那個類(偷龍轉鳳). 只要我們懂得Runtime的原理, 這一切都只是掩耳盜鈴罷了.
以下實現是參考Glow 技術團隊部落格的文章進行修改而成, 主要目的是加深對runtime的理解, 大家看完後不妨自己動手實現以下, 學而時習之, 不亦樂乎
KVO的缺陷
Apple給我們提供的KVO不能通過block來回撥處理, 只能通過下面這個方法來處理, 如果監聽的屬性多了, 或者監聽了多個物件的屬性, 那麼這裡就痛苦了, 要一直判斷判斷if else if else….多麻煩啊, 說實話我也不懂為什麼Apple不提供多一個傳block引數的方法
1 |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context |
那麼, 既然Apple沒有幫我們實現, 那我們就手動實現一個唄, 先看下我們最終目標是什麼樣的 :
1 2 3 4 5 6 |
[object jr_addObserver:observer key:@"name" callback:^(id observer, NSString *key, id oldValue, id newValue) { // do something here }]; [object jr_addObserver:observer key:@"address" callback:^(id observer, NSString *key, id oldValue, id newValue) { // do something here }]; |
簡簡單單就能讓observer監聽object的兩個屬性, 並且監聽屬性改變後的回撥就在對應的callback下, 清晰明瞭, 何不快哉! Talk is cheep, show you the code!
首先, 我們為NSObject
新增一個分類
NSObject+jr_KVO.h
1 2 3 4 5 6 7 8 9 10 |
#import #import "JRObserverInfo.h" @interface NSObject (jr_KVO) - (void)jr_addObserver:(id)observer key:(NSString *)key callback:(JRKVOCallback)callback; - (void)jr_removeObserver:(id)observer key:(NSString *)key; @end |
新增觀察者
在jr_addObserver
方法裡我們需要做什麼呢?
- 檢查物件是否存在該屬性的setter方法, 沒有的話我們就做什麼都白搭了, 既然別人都不允許你修改值了, 那也就不存在監聽值改變的事了
- 檢查自己(self)是不是一個kvo_class(如果該物件不是第一次被監聽屬性, 那麼它就是kvo_class, 反之則是原class), 如果是, 則跳過這一步; 如果不是, 則要修改self的類(origin_class -> kvo_class)
- 經過第二部, 到了這裡已經100%確定self是kvo_class的物件了, 那麼我們現在就要重寫kvo_class物件的對應屬性的setter方法
- 最後, 將觀察者物件(observer), 監聽的屬性(key), 值改變時的回撥block(callback), 用一個模型(
JRObserverInfo
)存進來, 然後利用關聯物件維護self的一個陣列(NSMutableArray *)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
- (void)jr_addObserver:(id)observer key:(NSString *)key callback:(JRKVOCallback)callback { // 1. 檢查物件的類有沒有相應的 setter 方法。如果沒有丟擲異常 SEL setterSelector = NSSelectorFromString([self setterForGetter:key]); Method setterMethod = class_getInstanceMethod([self class], setterSelector); if (!setterMethod) { NSLog(@"找不到該方法"); // throw exception here } // 2. 檢查物件 isa 指向的類是不是一個 KVO 類。如果不是,新建一個繼承原來類的子類,並把 isa 指向這個新建的子類 Class clazz = object_getClass(self); NSString *className = NSStringFromClass(clazz); if (![className hasPrefix:JRKVOClassPrefix]) { clazz = [self jr_KVOClassWithOriginalClassName:className]; object_setClass(self, clazz); } // 到這裡為止, object的類已不是原類了, 而是KVO新建的類 // 例如, Person -> JRKVOClassPrefixPerson // JRKVOClassPrefix是一個巨集, = @"JRKVO_" // 3. 為kvo class新增setter方法的實現 const char *types = method_getTypeEncoding(setterMethod); class_addMethod(clazz, setterSelector, (IMP)jr_setter, types); // 4. 新增該觀察者到觀察者列表中 // 4.1 建立觀察者的資訊 JRObserverInfo *info = [[JRObserverInfo alloc] initWithObserver:observer key:key callback:callback]; // 4.2 獲取關聯物件(裝著所有監聽者的陣列) NSMutableArray *observers = objc_getAssociatedObject(self, JRAssociateArrayKey); if (!observers) { observers = [NSMutableArray array]; objc_setAssociatedObject(self, JRAssociateArrayKey, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [observers addObject:info]; } |
這段程式碼還有幾個方法, 我們下面一一解釋…
首先, setterForGetter
和 getterForSetter
, 這兩個方法好辦. 第一個就是根據getter方法名獲得對應的setter方法名, 第二個就是根據setter方法名獲得對應的getter方法名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
- (NSString *)setterForGetter:(NSString *)key { // name -> Name -> setName: // 1. 首字母轉換成大寫 unichar c = [key characterAtIndex:0]; NSString *str = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c-32]]; // 2. 最前增加set, 最後增加: NSString *setter = [NSString stringWithFormat:@"set%@:", str]; return setter; } - (NSString *)getterForSetter:(NSString *)key { // setName: -> Name -> name // 1. 去掉set NSRange range = [key rangeOfString:@"set"]; NSString *subStr1 = [key substringFromIndex:range.location + range.length]; // 2. 首字母轉換成大寫 unichar c = [subStr1 characterAtIndex:0]; NSString *subStr2 = [subStr1 stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c+32]]; // 3. 去掉最後的: NSRange range2 = [subStr2 rangeOfString:@":"]; NSString *getter = [subStr2 substringToIndex:range2.location]; return getter; } |
這裡需要注意的是, 首字母轉換成大寫這一項, 不能直接呼叫NSString的capitalizedString
方法, 因為該方法返回的是除了首字母大寫之外其他字母全部小寫的字串.
然後, 接下來就是jr_KVOClassWithOriginalClassName:
方法了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
- (Class)jr_KVOClassWithOriginalClassName:(NSString *)className { // 生成kvo_class的類名 NSString *kvoClassName = [JRKVOClassPrefix stringByAppendingString:className]; Class kvoClass = NSClassFromString(kvoClassName); // 如果kvo class已經被註冊過了, 則直接返回 if (kvoClass) { return kvoClass; } // 如果kvo class不存在, 則建立這個類 Class originClass = object_getClass(self); kvoClass = objc_allocateClassPair(originClass, kvoClassName.UTF8String, 0); // 修改kvo class方法的實現, 學習Apple的做法, 隱瞞這個kvo_class Method clazzMethod = class_getInstanceMethod(kvoClass, @selector(class)); const char *types = method_getTypeEncoding(clazzMethod); class_addMethod(kvoClass, @selector(class), (IMP)jr_class, types); // 註冊kvo_class objc_registerClassPair(kvoClass); return kvoClass; } |
這個方法還是很直觀明瞭的, 可能不太明白的是為什麼要為kvo_class這個類重寫class
方法呢? 原因是我們要把這個kvo_class隱藏掉, 讓別人覺得自己的類沒有發生過任何改變, 以前是Person, 新增觀察者之後還是Person, 而不是KVO_Person.
這個jr_class
實現也很簡單.
1 2 3 4 5 6 |
Class jr_class(id self, SEL cmd) { Class clazz = object_getClass(self); // kvo_class Class superClazz = class_getSuperclass(clazz); // origin_class return superClazz; // origin_class } |
最後, 重頭戲來了, 那就是重寫kvo_class的setter
方法! Observing也正正是在這裡體現出來的.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/** * 重寫setter方法, 新方法在呼叫原方法後, 通知每個觀察者(呼叫傳入的block) */ static void jr_setter(id self, SEL _cmd, id newValue) { NSString *setterName = NSStringFromSelector(_cmd); NSString *getterName = [self getterForSetter:setterName]; if (!getterName) { NSLog(@"找不到getter方法"); // throw exception here } // 獲取舊值 id oldValue = [self valueForKey:getterName]; // 呼叫原類的setter方法 struct objc_super superClazz = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)) }; // 這裡需要做個型別強轉, 否則會報too many argument的錯誤 ((void (*)(void *, SEL, id))objc_msgSendSuper)(&superClazz, _cmd, newValue); // 為什麼不能用下面方法代替上面方法? // ((void (*)(id, SEL, id))objc_msgSendSuper)(self, _cmd, newValue); // 找出觀察者的陣列, 呼叫對應物件的callback NSMutableArray *observers = objc_getAssociatedObject(self, JRAssociateArrayKey); // 遍歷陣列 for (JRObserverInfo *info in observers) { if ([info.key isEqualToString:getterName]) { // gcd非同步呼叫callback dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ info.callback(info.observer, getterName, oldValue, newValue); }); } } } |
臥槽, struct objc_super
是什麼玩意, 臥槽, ((void (*)(void *, SEL, id))objc_msgSendSuper)(&superClazz, _cmd, newValue);
這一大串又是什麼玩意???
首先, 我們來看看objc_msgSend
與objc_msgSendSuper
的區別 :
1 2 3 |
Apple文件中是這麼說的 : void objc_msgSend(void /* id self, SEL op, ... */) void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */) |
那麼, 很顯然, 我們呼叫objc_msgSendSuper
的時候, 第一個引數已經不一樣了, 他接受的是一個指向結構體的指標, 於是才有了我們上面廢力氣建立的一個看似無用結構體
另外, 呼叫objc_msgSend
總是需要做方法的型別強轉,
1 2 3 4 |
objc_msgSendSuper(&superClazz, _cmd, newValue); // 當你這樣做時, 編譯器會報以下錯誤 /* Too many arguments to function call, expected 0, have 3 */ // 所以我們需要做個方法型別的強轉, 就不會報錯了 |
移除監聽者
移除監聽者就easy easy easy太多了, 直接上程式碼吧
1 2 3 4 5 6 7 8 9 10 11 12 |
- (void)jr_removeObserver:(id)observer key:(NSString *)key { NSMutableArray *observers = objc_getAssociatedObject(self, JRAssociateArrayKey); if (!observers) return; for (JRObserverInfo *info in observers) { if([info.key isEqualToString:key]) { [observers removeObject:info]; break; } } } |
相信不用註釋大家也能看懂, 大家記得在物件- dealloc
方法中呼叫該方法移除監聽者就OK了, 否則有可能報野指標錯誤, 訪問壞記憶體.
監聽者資訊
JRObserverInfo
是個什麼模型呢? 這裡告訴大家…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 回撥block大家可以自行定義 typedef void (^JRKVOCallback)(id observer, NSString *key, id oldValue, id newValue); @interface JRObserverInfo : NSObject /** 監聽者 */ @property (nonatomic, weak) id observer; /** 監聽的屬性 */ @property (nonatomic, copy) NSString *key; /** 回撥的block */ @property (nonatomic, copy) JRKVOCallback callback; - (instancetype)initWithObserver:(id)observer key:(NSString *)key callback:(JRKVOCallback)callback; @end |
執行展示
這裡我就簡單做個展示, 下面的textLabel監聽上面colorView背景色的改變, 點選button, 改變上面colorView的顏色, 然後textLabel輸出colorView的當前色
demo可在JRCustomKVODemo這裡下載, 同時歡迎大家關注我的Github, 覺得有幫助的話還請給個star~~
參考 :
如何自己動手實現KVO
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式