iOS-手動(dòng)實(shí)現(xiàn)KVO

我的Github地址 : Jerry4me, 本文章的demo鏈接 : JRCustomKVODemo


前言

KVO(Key-Value Observing, 鍵值觀察), KVO的實(shí)現(xiàn)也依賴于runtime. 當(dāng)你對(duì)一個(gè)對(duì)象進(jìn)行觀察時(shí), 系統(tǒng)會(huì)動(dòng)態(tài)創(chuàng)建一個(gè)類繼承自原類, 然后重寫被觀察屬性的setter方法. 然后重寫的setter方法會(huì)負(fù)責(zé)在調(diào)用原setter方法前后通知觀察者. KVO還會(huì)修改原對(duì)象的isa指針指向這個(gè)新類.

我們知道, 對(duì)象是通過isa指針去查找自己是屬于哪個(gè)類, 并去所在類的方法列表中查找方法的, 所以這個(gè)時(shí)候這個(gè)對(duì)象就自然地變成了新類的實(shí)例對(duì)象.

不僅如此, Apple還重寫了原類的- class方法, 視圖欺騙我們, 這個(gè)類沒有變, 還是原來的那個(gè)類(偷龍轉(zhuǎn)鳳). 只要我們懂得Runtime的原理, 這一切都只是掩耳盜鈴罷了.

以下實(shí)現(xiàn)是參考Glow 技術(shù)團(tuán)隊(duì)博客的文章進(jìn)行修改而成, 主要目的是加深對(duì)runtime的理解, 大家看完后不妨自己動(dòng)手實(shí)現(xiàn)以下, 學(xué)而時(shí)習(xí)之, 不亦樂乎


KVO的缺陷

Apple給我們提供的KVO不能通過block來回調(diào)處理, 只能通過下面這個(gè)方法來處理, 如果監(jiān)聽的屬性多了, 或者監(jiān)聽了多個(gè)對(duì)象的屬性, 那么這里就痛苦了, 要一直判斷判斷if else if else....多麻煩啊, 說實(shí)話我也不懂為什么Apple不提供多一個(gè)傳block參數(shù)的方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

那么, 既然Apple沒有幫我們實(shí)現(xiàn), 那我們就手動(dòng)實(shí)現(xiàn)一個(gè)唄, 先看下我們最終目標(biāo)是什么樣的 :

[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
}];

簡(jiǎn)簡(jiǎn)單單就能讓observer監(jiān)聽object的兩個(gè)屬性, 并且監(jiān)聽屬性改變后的回調(diào)就在對(duì)應(yīng)的callback下, 清晰明了, 何不快哉! Talk is cheap, show you the code!


首先, 我們?yōu)?code>NSObject新增一個(gè)分類

NSObject+jr_KVO.h

#import <Foundation/Foundation.h>
#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方法里我們需要做什么呢?

  1. 檢查對(duì)象是否存在該屬性的setter方法, 沒有的話我們就做什么都白搭了, 既然別人都不允許你修改值了, 那也就不存在監(jiān)聽值改變的事了
  2. 檢查自己(self)是不是一個(gè)kvo_class(如果該對(duì)象不是第一次被監(jiān)聽屬性, 那么它就是kvo_class, 反之則是原class), 如果是, 則跳過這一步; 如果不是, 則要修改self的類(origin_class -> kvo_class)
  3. 經(jīng)過第二部, 到了這里已經(jīng)100%確定self是kvo_class的對(duì)象了, 那么我們現(xiàn)在就要重寫kvo_class對(duì)象的對(duì)應(yīng)屬性的setter方法
  4. 最后, 將觀察者對(duì)象(observer), 監(jiān)聽的屬性(key), 值改變時(shí)的回調(diào)block(callback), 用一個(gè)模型(JRObserverInfo)存進(jìn)來, 然后利用關(guān)聯(lián)對(duì)象維護(hù)self的一個(gè)數(shù)組(NSMutableArray<JRObserverInfo *> *)
- (void)jr_addObserver:(id)observer key:(NSString *)key callback:(JRKVOCallback)callback
{
    // 1. 檢查對(duì)象的類有沒有相應(yīng)的 setter 方法片吊。如果沒有拋出異常
    SEL setterSelector = NSSelectorFromString([self setterForGetter:key]);
    
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        NSLog(@"找不到該方法");
        // throw exception here
    }
    
    // 2. 檢查對(duì)象 isa 指向的類是不是一個(gè) KVO 類缚窿。如果不是,新建一個(gè)繼承原來類的子類蘸秘,并把 isa 指向這個(gè)新建的子類
    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是一個(gè)宏, = @"JRKVO_"
    
    // 3. 為kvo class添加setter方法的實(shí)現(xiàn)
    const char *types = method_getTypeEncoding(setterMethod);
    class_addMethod(clazz, setterSelector, (IMP)jr_setter, types);
    
    // 4. 添加該觀察者到觀察者列表中
    // 4.1 創(chuàng)建觀察者的信息
    JRObserverInfo *info = [[JRObserverInfo alloc] initWithObserver:observer key:key callback:callback];
    // 4.2 獲取關(guān)聯(lián)對(duì)象(裝著所有監(jiān)聽者的數(shù)組)
    NSMutableArray *observers = objc_getAssociatedObject(self, JRAssociateArrayKey);
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, JRAssociateArrayKey, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    [observers addObject:info];
    
}

這段代碼還有幾個(gè)方法, 我們下面一一解釋...

首先, setterForGettergetterForSetter, 這兩個(gè)方法好辦. 第一個(gè)就是根據(jù)getter方法名獲得對(duì)應(yīng)的setter方法名, 第二個(gè)就是根據(jù)setter方法名獲得對(duì)應(yīng)的getter方法名

- (NSString *)setterForGetter:(NSString *)key
{
    // name -> Name -> setName:
    
    // 1. 首字母轉(zhuǎn)換成大寫
    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. 首字母轉(zhuǎn)換成大寫
    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;
}

這里需要注意的是, 首字母轉(zhuǎn)換成大寫這一項(xiàng), 不能直接調(diào)用NSString的capitalizedString方法, 因?yàn)樵摲椒ǚ祷氐氖浅耸鬃帜复髮懼馄渌帜溉啃懙淖址?

然后, 接下來就是jr_KVOClassWithOriginalClassName:方法了

- (Class)jr_KVOClassWithOriginalClassName:(NSString *)className
{
    // 生成kvo_class的類名
    NSString *kvoClassName = [JRKVOClassPrefix stringByAppendingString:className];
    Class kvoClass = NSClassFromString(kvoClassName);
    
    // 如果kvo class已經(jīng)被注冊(cè)過了, 則直接返回
    if (kvoClass) {
        return kvoClass;
    }
    
    // 如果kvo class不存在, 則創(chuàng)建這個(gè)類
    Class originClass = object_getClass(self);
    kvoClass = objc_allocateClassPair(originClass, kvoClassName.UTF8String, 0);
    
    // 修改kvo class方法的實(shí)現(xiàn), 學(xué)習(xí)Apple的做法, 隱瞞這個(gè)kvo_class
    Method clazzMethod = class_getInstanceMethod(kvoClass, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClass, @selector(class), (IMP)jr_class, types);
    
    // 注冊(cè)kvo_class
    objc_registerClassPair(kvoClass);
    
    return kvoClass;
    
}

這個(gè)方法還是很直觀明了的, 可能不太明白的是為什么要為kvo_class這個(gè)類重寫class方法呢? 原因是我們要把這個(gè)kvo_class隱藏掉, 讓別人覺得自己的類沒有發(fā)生過任何改變, 以前是Person, 添加觀察者之后還是Person, 而不是KVO_Person.
這個(gè)jr_class實(shí)現(xiàn)也很簡(jiǎn)單.

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也正正是在這里體現(xiàn)出來的.

    /**
     *  重寫setter方法, 新方法在調(diào)用原方法后, 通知每個(gè)觀察者(調(diào)用傳入的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];
    
    // 調(diào)用原類的setter方法
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    // 這里需要做個(gè)類型強(qiáng)轉(zhuǎn), 否則會(huì)報(bào)too many argument的錯(cuò)誤
    ((void (*)(void *, SEL, id))objc_msgSendSuper)(&superClazz, _cmd, newValue);
    // 為什么不能用下面方法代替上面方法?
    //    ((void (*)(id, SEL, id))objc_msgSendSuper)(self, _cmd, newValue);
    
    // 找出觀察者的數(shù)組, 調(diào)用對(duì)應(yīng)對(duì)象的callback
    NSMutableArray *observers = objc_getAssociatedObject(self, JRAssociateArrayKey);
    // 遍歷數(shù)組
    for (JRObserverInfo *info in observers) {
        if ([info.key isEqualToString:getterName]) {
            // gcd異步調(diào)用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_msgSendobjc_msgSendSuper的區(qū)別 :

Apple文檔中是這么說的 : 
void objc_msgSend(void /* id self, SEL op, ... */)
void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */)

那么, 很顯然, 我們調(diào)用objc_msgSendSuper的時(shí)候, 第一個(gè)參數(shù)已經(jīng)不一樣了, 他接受的是一個(gè)指向結(jié)構(gòu)體的指針, 于是才有了我們上面廢力氣創(chuàng)建的一個(gè)看似無用結(jié)構(gòu)體

另外, 調(diào)用objc_msgSend總是需要做方法的類型強(qiáng)轉(zhuǎn),

objc_msgSendSuper(&superClazz, _cmd, newValue);
// 當(dāng)你這樣做時(shí), 編譯器會(huì)報(bào)以下錯(cuò)誤
/* Too many arguments to function call, expected 0, have 3 */
// 所以我們需要做個(gè)方法類型的強(qiáng)轉(zhuǎn), 就不會(huì)報(bào)錯(cuò)了

移除監(jiān)聽者

移除監(jiān)聽者就easy easy easy太多了, 直接上代碼吧

- (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;
        }
    }
}

相信不用注釋大家也能看懂, 大家記得在對(duì)象- dealloc方法中調(diào)用該方法移除監(jiān)聽者就OK了, 否則有可能報(bào)野指針錯(cuò)誤, 訪問壞內(nèi)存.


監(jiān)聽者信息

JRObserverInfo是個(gè)什么模型呢? 這里告訴大家...

// 回調(diào)block大家可以自行定義
typedef void (^JRKVOCallback)(id observer, NSString *key, id oldValue, id newValue);

@interface JRObserverInfo : NSObject

/** 監(jiān)聽者 */
@property (nonatomic, weak) id observer;
/** 監(jiān)聽的屬性 */
@property (nonatomic, copy) NSString *key;
/** 回調(diào)的block */
@property (nonatomic, copy) JRKVOCallback callback;

- (instancetype)initWithObserver:(id)observer key:(NSString *)key callback:(JRKVOCallback)callback;
@end

運(yùn)行展示

這里我就簡(jiǎn)單做個(gè)展示, 下面的textLabel監(jiān)聽上面colorView背景色的改變, 點(diǎn)擊button, 改變上面colorView的顏色, 然后textLabel輸出colorView的當(dāng)前色

運(yùn)行結(jié)果

demo可在JRCustomKVODemo這里下載, 同時(shí)歡迎大家關(guān)注我的Github, 覺得有幫助的話還請(qǐng)給個(gè)star~~


參考 :
如何自己動(dòng)手實(shí)現(xiàn)KVO

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市滤港,隨后出現(xiàn)的幾起案子曲饱,更是在濱河造成了極大的恐慌,老刑警劉巖防症,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件孟辑,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蔫敲,警方通過查閱死者的電腦和手機(jī)饲嗽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奈嘿,“玉大人貌虾,你說我怎么就攤上這事∪褂蹋” “怎么了尽狠?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵衔憨,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我袄膏,道長(zhǎng)践图,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任沉馆,我火速辦了婚禮平项,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘悍及。我一直安慰自己闽瓢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布心赶。 她就那樣靜靜地躺著扣讼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缨叫。 梳的紋絲不亂的頭發(fā)上椭符,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音耻姥,去河邊找鬼销钝。 笑死,一個(gè)胖子當(dāng)著我的面吹牛琐簇,可吹牛的內(nèi)容都是我干的蒸健。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼婉商,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼似忧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起丈秩,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤盯捌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蘑秽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饺著,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年肠牲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了幼衰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡埂材,死狀恐怖塑顺,靈堂內(nèi)的尸體忽然破棺而出汤求,到底是詐尸還是另有隱情俏险,我是刑警寧澤严拒,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站竖独,受9級(jí)特大地震影響裤唠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜莹痢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一种蘸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竞膳,春花似錦航瞭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至锉走,卻和暖如春滨彻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背挪蹭。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工亭饵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人梁厉。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓辜羊,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親词顾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子只冻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

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

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,709評(píng)論 0 9
  • 前言 KVO:簡(jiǎn)單的來說计技,就是觀察者觀察被觀察對(duì)象屬性的變化而發(fā)生相應(yīng)的變化喜德。實(shí)現(xiàn)的原理基于KVC與強(qiáng)大的Runt...
    Colleny_Z閱讀 1,279評(píng)論 0 2
  • 本篇會(huì)對(duì)KVO的實(shí)現(xiàn)進(jìn)行探究,不涉及太多KVO的使用方法垮媒,但是會(huì)有一些使用時(shí)的思考舍悯。 一、使用上的疑問 1.key...
    奮拓達(dá)閱讀 505評(píng)論 0 2
  • 尋秦記 1 旅客們步履匆忙走出車站睡雇,繞過灰色城墻萌衬,消失在車流人海中。他們腳步帶起的微塵還在身后升騰它抱。這是西安的塵土...
    九曲胡同閱讀 961評(píng)論 9 8
  • 早上醒來,舍友A問我:“你還要一個(gè)人啊”混移?我一頭霧水祠墅,是在跟我說話嗎? A小姐一臉嫌棄:“今天是520歌径,大姐毁嗦。” ...
    會(huì)飛的小蝸牛閱讀 507評(píng)論 0 5