利用 runtime & block 方式實(shí)現(xiàn) KVO。

上一篇博客中,說(shuō)明了 KVO 的執(zhí)行過(guò)程和基本的實(shí)現(xiàn)原理柏肪。

KVO的執(zhí)行原理

  1. 對(duì)象本身作為事件的發(fā)布者,在自己被觀察者(通常是那個(gè)包含自己的控制器)觀察的屬性發(fā)生改變的時(shí)候然遏,向觀察者發(fā)布屬性修改了的通知逊谋。(本質(zhì)就是在 setter 方法調(diào)用的時(shí)候執(zhí)行發(fā)布)
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
  1. 控制器,本身之前實(shí)現(xiàn)訂閱好這個(gè) setter 通知绢淀,并在事件響應(yīng)函數(shù)中萤悴,對(duì)這個(gè)的事件發(fā)布做出相應(yīng)。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@"%@",change);
}

這些道理都懂皆的,但如何自己在了解了 KVO 實(shí)現(xiàn)原理之后覆履,自己實(shí)現(xiàn)一個(gè) KVO 呢?

為什么要自己實(shí)現(xiàn) KVO费薄?

  1. 熟悉 runtime 的各種使用方法硝全。
  2. 傳統(tǒng)的 KVO,所有的屬性訂閱都幾種在了一個(gè)方法相應(yīng)體里面楞抡∥爸冢看起來(lái)不太優(yōu)雅(裝逼)
  if ([object isKindOfClass:[xxx class]] && [keyPath isEqualToString:@"xxx"]) {
        
    } else if ([object isKindOfClass:[xxx class]] && [keyPath isEqualToString:@"xxx"]) {
        
    } else if ([object isKindOfClass:[xxx class]] && [keyPath isEqualToString:@"xxx"]) {
        
    } ......

實(shí)現(xiàn)目標(biāo)

希望能夠以 block 回調(diào)的方式來(lái)自己實(shí)現(xiàn) KVO。
好處在于召廷,每一個(gè)屬性對(duì)應(yīng)一個(gè)自己的回調(diào) block凳厢。
這樣代碼看起來(lái)比較清晰和緊湊账胧。
也符合函數(shù)是編程的思想。

最終需要實(shí)現(xiàn)的代碼效果

  [self.person rl_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil withBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
        NSLog(@"observer : %@ keyPath : %@ oldValue : %@ newValue : %@",observer,keyPath,oldValue,newValue);
    }];
    
    [self.person rl_addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew) context:nil withBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
       //... 這是 age 的改變時(shí)的 KVO block 回調(diào)
    }];
    
    .....

在開(kāi)始先自己的目標(biāo)方法之前先紫,首先在回顧一下 KVO 的實(shí)現(xiàn)基本步驟

  1. 創(chuàng)建一個(gè)當(dāng)前類的子類治泥。
  2. 在子類中重寫(xiě)當(dāng)前屬性的 setter 方法。
  3. 在子類重寫(xiě)的 setter 方法里面遮精,手動(dòng)的調(diào)用 KVO 實(shí)現(xiàn)的兩句代碼居夹。
image.png

當(dāng)然,和真實(shí)的 KVO 相比仑鸥,這里缺少了 _isKVO , class , dealloc 的實(shí)現(xiàn)吮播。

首要目的,先是以函數(shù)式編程的方法眼俊,來(lái)實(shí)現(xiàn)自己的 KVO意狠。


開(kāi)始動(dòng)手來(lái)實(shí)現(xiàn)自己的 KVO。

第一步:添加一個(gè) NSObject 的分類

typedef void(^KVOBlock)(id observer,NSString *keyPath,id oldValue ,id newValue);

@interface NSObject (RLKVO)

- (void)rl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context withBlock:(KVOBlock)block;

@end

方法名哪來(lái)的疮胖?
復(fù)制粘貼系統(tǒng)自帶的 KVO 的方法环戈。
后面加入了一個(gè) block用來(lái)當(dāng)屬性發(fā)生改變時(shí),回調(diào)觀察者澎灸。

第二步:實(shí)現(xiàn)這個(gè)方法

先考慮一下院塞,在這個(gè)方法內(nèi)部,需要做什么事情性昭。

  1. keyPath 的有效性檢查拦止。
  2. 根據(jù)當(dāng)前 self,創(chuàng)建一個(gè)子類糜颠。
  3. 當(dāng)類創(chuàng)建完畢之后汹族,修改當(dāng)前 self 的 isa 指針到這個(gè)子類。
  4. 在子類中其兴,添加一個(gè) set方法顶瞒,用來(lái)重寫(xiě)父類的 setter 方法。(用 class_addMethod 添加方法 sel 和 真實(shí)的函數(shù)名元旬,不用匹配榴徐,只要建立雙方的聯(lián)系就行了。此例子中匀归,sel = setName: 而真實(shí)的 C 函數(shù)叫 kvoSetter)坑资。sel & IMP 不需要匹配名字
  5. 在子類的 setter 方法內(nèi)部,調(diào)用 self willChangeValueForKey: & self didChangeValueForkey:穆端。

當(dāng)然盐茎,還有有一些細(xì)枝末節(jié)的東西。在代碼注釋里會(huì)有說(shuō)明徙赢。

- (void)rl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context withBlock:(KVOBlock)block {
    // keyPath 的檢查字柠。
    NSString *setterName = setterFormat(keyPath);
    SEL sel = NSSelectorFromString(setterName);
    // 這里獲取 method 的意義在于,可以方便的獲取當(dāng)前方法的 EncodingType 一遍在添加方法class_addMethod的時(shí)候狡赐,可以設(shè)置 EncodingType.
    Method setterMethod = class_getInstanceMethod([self class], sel);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%@ have not %@ method",[self class],setterName] userInfo:nil];
    }
    // 獲取前類的類名窑业,并作為之后創(chuàng)建子類的父類。
    NSString *superClassName = NSStringFromClass([self class]);
    const char *type = method_getTypeEncoding(setterMethod); // 通過(guò)方法結(jié)果提枕屉,拿到方法編碼常柄。EncodingType。否則就需要自己手拼寫(xiě)  v@:@ 麻煩搀擂。
    
    // 動(dòng)態(tài)創(chuàng)建類
    Class newClass = [self createClassFromSuperName:superClassName sel:sel encodingType:type];
    
    // 替換當(dāng)前 self 的 isa 指針
    object_setClass(self, newClass);
    
    // 保存信息
    RLKVO_Info *info = [[RLKVO_Info alloc] initWithObserver:observer block:block keypath:keyPath];
    
    NSMutableArray *infoArray = objc_getAssociatedObject(self, &infoArrayPro);
    if (!infoArray) {
        infoArray = [NSMutableArray array];
        objc_setAssociatedObject(self, &infoArrayPro, infoArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    [infoArray addObject:info];
    
    
}

此段代碼解釋:

  1. 首先根據(jù)用戶傳入的 keyPath 字符串西潘,配置成 setKeyPath: 字符串,表示 setter 方法的sel哨颂。
  2. 獲取這個(gè) setKeyPath: 的 sel
  3. 根據(jù) sel 獲取當(dāng)期對(duì)應(yīng)的方法 Method
  4. 如果 Method 不存在喷市,說(shuō)明傳入的 keyPath 是錯(cuò)誤的。對(duì)象沒(méi)有這個(gè)屬性威恼,拋出異常品姓。
  5. 獲取當(dāng)前對(duì)象的 className,以備創(chuàng)建子類的時(shí)候使用箫措。
  6. 根據(jù) Method 拿到方法的 EncodingType 簽名腹备。
  7. 動(dòng)態(tài)的創(chuàng)建子類。

創(chuàng)建當(dāng)前類的子類方法

/**
 根據(jù)父類創(chuàng)建子類
 
 @param superName 父類類型字符串
 @param sel 父類當(dāng)前方法的 sel
 @param encodingType  父類當(dāng)前方法的 EncodingType
 @return  返回以當(dāng)前父類創(chuàng)建的新類斤蔓。
 */
- (Class)createClassFromSuperName:(NSString *)superName sel:(SEL)sel encodingType:(const char *)encodingType {
    
    // 創(chuàng)建一個(gè)類.
    Class newClass = objc_allocateClassPair(
                                            NSClassFromString(superName), // 當(dāng)前類的基類
                                            [NSString stringWithFormat:@"RLKVO_%@",
                                             NSStringFromClass([self class])].UTF8String,// 類名
                                            0);
    
    
    // const char *types = method_getTypeEncoding(class_getInstanceMethod(NSClassFromString(superName), @selector(class)));
    
    // 往新類中添加方法
    class_addMethod(newClass, sel, (IMP)kvoSetter, encodingType);
    
    
    // 類創(chuàng)建完畢之后植酥,注冊(cè)到 runtime
    objc_registerClassPair(newClass);
    
    
    // 返回這個(gè)新類。
    return newClass;
}


此段代碼說(shuō)明:

  1. 動(dòng)態(tài)的創(chuàng)建一個(gè)子類弦牡,類名為 RLKVO_父類的名字友驮。
  2. 往新類中,添加一個(gè) kvoSetter 的方法喇伯,方法簽名使用父類的 setKeyPath: 喊儡。
  3. 新類創(chuàng)建完畢,還并沒(méi)有完成稻据,需要把新類注冊(cè)到 runtime艾猜。
  4. 最后返回已經(jīng)注入到 runtime 的這個(gè)子類。

當(dāng)子類創(chuàng)建捻悯,完畢之后匆赃,就需要把當(dāng)前對(duì)象的 isa 指針改成新創(chuàng)建的這個(gè)類了。
也就是第一段代碼的今缚。

// 動(dòng)態(tài)創(chuàng)建類
    Class newClass = [self createClassFromSuperName:superClassName sel:sel encodingType:type];
    
    // 替換當(dāng)前 self 的 isa 指針
    object_setClass(self, newClass);

到目前為止算柳,做的事情,主要包括創(chuàng)建了一個(gè)基于當(dāng)前類的子類 & 把當(dāng)前類的 isa 指針改成了子類姓言。

往子類中添加的 kvoSetter 方法具體實(shí)現(xiàn)

// 使用 class_addMethod runtime 添加方法瞬项。
void kvoSetter(id self,SEL _cmd,id newValue) {
    // 拿到 setter
    NSString *setterName = NSStringFromSelector(_cmd);
    // 根據(jù) setter 拿到 getter
    NSString *getterName = getterFormat(setterName);
    
    id oldValue = [self valueForKey:getterName];
    
    if (!getterName) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"getter 方法不存在" userInfo: nil];
    }
    
    // 手動(dòng)開(kāi)啟 KVO
    [self willChangeValueForKey:getterName];
    // 調(diào)用父類的方法蔗蹋。
    // 定義一個(gè)函數(shù)指針
    void(*objc_msgSendRLKVO)(void *,SEL ,id) = (void *)objc_msgSendSuper;
    
    struct objc_super superClassStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    objc_msgSendRLKVO(&superClassStruct,_cmd,newValue);
    
    [self didChangeValueForKey:getterName];
    
    
    //
    
    NSMutableArray *infoArrM = objc_getAssociatedObject(self, &infoArrayPro);
    if (infoArrM) {
        [infoArrM enumerateObjectsUsingBlock:^(RLKVO_Info *info, NSUInteger idx, BOOL * _Nonnull stop) {
            info.block(info.observer, info.keyPath, oldValue, newValue);
        }];
    }
}

在方法實(shí)現(xiàn)中,最重要的就是需要調(diào)用 block囱淋,把 KVO 的相關(guān)數(shù)據(jù)回調(diào)出去給觀察者猪杭。

 NSMutableArray *infoArrM = objc_getAssociatedObject(self, &infoArrayPro);
    if (infoArrM) {
        [infoArrM enumerateObjectsUsingBlock:^(RLKVO_Info *info, NSUInteger idx, BOOL * _Nonnull stop) {
            info.block(info.observer, info.keyPath, oldValue, newValue);
        }];
    }

最后運(yùn)行結(jié)果:

- (void)viewDidLoad {
    [super viewDidLoad];
    _person = [RLPerson new];
    _person.name = @"李四";
    [_person rl_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil withBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
        NSLog(@"keyPath : %@ oldValue: %@ newValue : %@",keyPath,oldValue,newValue);
    }];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    _person.name = [NSString stringWithFormat:@"%@-",_person.name];
}
KVO+BLOCK 實(shí)現(xiàn)2.gif

此次實(shí)現(xiàn)過(guò)程中,代碼細(xì)節(jié)有很多妥衣,光使用文字很難說(shuō)的清楚皂吮。
我把源碼放到了 github 上面。有興趣的小伙伴可以下載來(lái)看看税手。

注意:此 demo 應(yīng)該不能使用到實(shí)際的開(kāi)發(fā)環(huán)境中蜂筹。還有很多細(xì)節(jié)沒(méi)有處理。比如 _iskVO class dealloc 等芦倒。僅供學(xué)習(xí)之用艺挪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市熙暴,隨后出現(xiàn)的幾起案子闺属,更是在濱河造成了極大的恐慌,老刑警劉巖周霉,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掂器,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡俱箱,警方通過(guò)查閱死者的電腦和手機(jī)国瓮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)狞谱,“玉大人乃摹,你說(shuō)我怎么就攤上這事「疲” “怎么了孵睬?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)伶跷。 經(jīng)常有香客問(wèn)我掰读,道長(zhǎng),這世上最難降的妖魔是什么叭莫? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任蹈集,我火速辦了婚禮,結(jié)果婚禮上雇初,老公的妹妹穿的比我還像新娘拢肆。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布郭怪。 她就那樣靜靜地躺著支示,像睡著了一般。 火紅的嫁衣襯著肌膚如雪移盆。 梳的紋絲不亂的頭發(fā)上悼院,一...
    開(kāi)封第一講書(shū)人閱讀 51,521評(píng)論 1 304
  • 那天,我揣著相機(jī)與錄音咒循,去河邊找鬼。 笑死绞愚,一個(gè)胖子當(dāng)著我的面吹牛叙甸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播位衩,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼裆蒸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了糖驴?” 一聲冷哼從身側(cè)響起僚祷,我...
    開(kāi)封第一講書(shū)人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贮缕,沒(méi)想到半個(gè)月后辙谜,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡感昼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年装哆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片定嗓。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蜕琴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宵溅,到底是詐尸還是另有隱情凌简,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布恃逻,位于F島的核電站雏搂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏辛块。R本人自食惡果不足惜畔派,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望润绵。 院中可真熱鬧线椰,春花似錦、人聲如沸尘盼。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至配紫,卻和暖如春径密,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背躺孝。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工享扔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人植袍。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓惧眠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親于个。 傳聞我的和親對(duì)象是個(gè)殘疾皇子氛魁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

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

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,715評(píng)論 0 9
  • 本篇會(huì)對(duì)KVO的實(shí)現(xiàn)進(jìn)行探究厅篓,不涉及太多KVO的使用方法秀存,但是會(huì)有一些使用時(shí)的思考。 一羽氮、使用上的疑問(wèn) 1.key...
    奮拓達(dá)閱讀 507評(píng)論 0 2
  • 面試題參考1 : 面試題[http://www.cocoachina.com/ios/20150803/12872...
    江河_ios閱讀 1,737評(píng)論 0 4
  • 二十幾歲是最美的年紀(jì)或链,我們有了賺錢(qián)能力,不再開(kāi)口向父母要錢(qián)乏苦,面對(duì)自己喜歡的物品可以憑借一己之力隨時(shí)購(gòu)買(mǎi)株扛,一定程度上...
    不傾城閱讀 678評(píng)論 0 5
  • 同學(xué)說(shuō)我是一個(gè)很有動(dòng)物緣的人,因?yàn)闊o(wú)論是狗還是貓汇荐,我若是喚他洞就,不一會(huì)兒他就會(huì)到我的懷里來(lái)。 可他們不知道的是掀淘,我最...
    艾米栗狂想曲閱讀 206評(píng)論 0 0