利用Runtime和函數(shù)響應(yīng)式編程自己實(shí)現(xiàn)OC的KVO

前面我們?cè)谑褂肂lock的時(shí)候提到了函數(shù)式編程和鏈?zhǔn)秸{(diào)用的用法嬉橙,但是實(shí)際上Block還有一種編程思想,就是響應(yīng)式編程。
函數(shù)式編程是把相關(guān)邏輯代碼寫到一起遂唧,鏈?zhǔn)秸{(diào)用是可以使用點(diǎn)語(yǔ)法不停的調(diào)用方法,而響應(yīng)式編程則是把事件回調(diào)邏輯和使用寫到一起吊奢,著名的RAC框架就是響應(yīng)式編程的代表盖彭。
不過不難看出,Block的靈活使用页滚,簡(jiǎn)化了我們代碼的復(fù)雜度召边,提升了我們編寫程序的效率。
今天裹驰,我們就利用之前所學(xué)過的Block和Runtime做一個(gè)有趣的事情隧熙,就是自己寫一個(gè)KVO,并且用函數(shù)式響應(yīng)式編程思想進(jìn)行一個(gè)改造幻林。

  • 什么是KVO

KVO即(Key - Value - Observer)贞盯,是觀察者模式的一種體現(xiàn)音念,它可以觀察對(duì)象的一個(gè)屬性,當(dāng)它發(fā)生改變的時(shí)候躏敢,觸發(fā)回調(diào)事件闷愤,方便我們進(jìn)行邏輯操作。
我們先使用一下KVO父丰,然后再對(duì)其進(jìn)行分析肝谭。這里,我們自己創(chuàng)建一個(gè)類Person蛾扇,給它一個(gè)屬性name攘烛,并且對(duì)它的name屬性進(jìn)行觀察。為了方便測(cè)試镀首,我們添加了一個(gè)按鈕用來(lái)修改它的名字坟漱。

KVO的使用步驟:

  1. 給對(duì)象添加觀察者
//初始化對(duì)象
self.person = [Person new];
//添加觀察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
  1. 實(shí)現(xiàn)回調(diào)方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@",[change objectForKey:NSKeyValueChangeNewKey]);
    }  
}
  1. 觸發(fā)回調(diào)事件
- (IBAction)changeName:(id)sender {
    NSString *defaultName = @"張三";
    self.person.name = [defaultName stringByAppendingFormat:@"%d",i++];
}
  1. 移除觀察者
- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
}

然后,我們點(diǎn)擊按鈕就可以看到不斷打印出的新name值了:

特別需要注意的一點(diǎn)是更哄,KVO只有在調(diào)用Setter方法的時(shí)候芋齿,才會(huì)進(jìn)行回調(diào),直接使用下劃線修改是不會(huì)觸發(fā)KVO回調(diào)的成翩。
接著觅捆,我們?cè)賮?lái)分析一下KVO的實(shí)現(xiàn)原理。KVO其實(shí)是用Runtime實(shí)現(xiàn)的麻敌,怎么證明呢栅炒?我們?cè)谔砑佑^察者那一行打一個(gè)斷點(diǎn),單步執(zhí)行后术羔,再看我們的self.person的類型:


可以看到赢赊,person變量的isa指針,指向的類型變成了NSKVONotifying_Person级历,這說(shuō)明KVO在運(yùn)行時(shí)改變了所觀察對(duì)象的類型释移,這個(gè)類是Person類的子類。我們可以通過這句代碼打印出它的父類來(lái)證明:

NSLog(@"%@",[NSString stringWithUTF8String:class_getName([objc_getClass("NSKVONotifying_Person") superclass])]);

前面我們知道只有Setter方法被調(diào)用才可以進(jìn)入回調(diào)寥殖,那說(shuō)明玩讳,這個(gè)子類一定重寫了父類屬性的Setter方法,并且在其中做了監(jiān)聽以及事件的處理扛禽。

明白了這些锋边,我們就可以自己利用Runtime去編寫一個(gè)KVO了。

首先编曼,KVO好像誰(shuí)都可以直接調(diào)用,不需要導(dǎo)入頭文件剩辟,我們因此可以推測(cè)掐场,它應(yīng)該是NSObject的一個(gè)Category往扔,我們也順著這個(gè)思路,創(chuàng)建一個(gè)NSObject的分類熊户,我們這里命名為CBX_KVO萍膛,并且用一個(gè)相同的方法(名字我們加一個(gè)前綴)來(lái)添加觀察者:

@interface NSObject (CBX_KVO)

- (void)CBX_addObserver:(nonnull NSObject *)observer forKeyPath:(nonnull NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

@end

接下來(lái),我們需要在方法的實(shí)現(xiàn)里面做一些事情了嚷堡,根據(jù)我們前面的分析蝗罗,我們要?jiǎng)討B(tài)創(chuàng)建一個(gè)新類,這個(gè)類是觀察對(duì)象的子類蝌戒,并且重寫它對(duì)應(yīng)屬性的Setter方法:

- (void)CBX_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    
    //創(chuàng)建一個(gè)觀察對(duì)象的子類
    NSString *oldName = NSStringFromClass(self.class);
    NSString *newName = [@"CBX_KVO_" stringByAppendingString:oldName];
    Class NewClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    //注冊(cè)新類
    objc_registerClassPair(NewClass);
    //改變self的類型
    object_setClass(self, NewClass);
    //為其添加一個(gè)Setter方法
    class_addMethod(NewClass, @selector(setName:), (IMP)newSetter, "v@:@");
}

void newSetter(NSString * newName){
    NSLog(@"%@",newName);
}

寫到這里串塑,我們可以先測(cè)試一下,添加我們自己的觀察者北苟,每次點(diǎn)擊按鈕改變name屬性的時(shí)候能不能打印出新的值:

[self.person CBX_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];


但是實(shí)際上桩匪,打印出來(lái)的并不是我們想要的改變以后的name值,而是這個(gè)對(duì)象自己友鼻,這是因?yàn)镺C方法都有兩個(gè)隱藏的參數(shù):self_cmd傻昙,我們自己寫的函數(shù)并沒有添加這兩個(gè)參數(shù),所以讀出來(lái)的默認(rèn)是第一個(gè)參數(shù)彩扔。
接下來(lái)妆档,我們改變一下函數(shù):

void newSetter(id self,SEL _cmd,NSString * newName){
    NSLog(@"%@",newName);
}

再測(cè)試一下:


現(xiàn)在可以順利打印出值了。然后我們就可以在自己寫的函數(shù)里利用Block回調(diào)來(lái)簡(jiǎn)化我們的KVO虫碉。
首先贾惦,我們需要在前面的CBX_addObserver方法添加一個(gè)Block參數(shù)來(lái)回調(diào)處理后續(xù)邏輯。
然后在修改了屬性以后蔗衡,在函數(shù)內(nèi)部調(diào)用Block纤虽,就可以實(shí)現(xiàn)回調(diào)的效果了。
但是怎么在我們寫的函數(shù)里面獲取Block呢绞惦?可以有兩種方法逼纸,第一種是把block當(dāng)做參數(shù)傳下去,另外一種就是把block作為屬性添加給新類济蝉,然后函數(shù)內(nèi)部可以通過self獲取并調(diào)用杰刽。我們這里使用第二種:

- (void)CBX_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block:(void (^)(NSString *newName))block{
    
    //創(chuàng)建一個(gè)觀察對(duì)象的子類
    NSString *oldName = NSStringFromClass(self.class);
    NSString *newName = [@"CBX_KVO_" stringByAppendingString:oldName];
    Class NewClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    //注冊(cè)新類
    objc_registerClassPair(NewClass);
    //改變self的類型
    object_setClass(self, NewClass);
    //為其添加一個(gè)Setter方法
    class_addMethod(NewClass, @selector(setName:), (IMP)newSetter, "v@:@");
    //為self增加block屬性
    objc_setAssociatedObject(self, @"1", block, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

void newSetter(id self,SEL _cmd,NSString * newName){
    //處理回調(diào)
    void(^block)(NSString *newname) = objc_getAssociatedObject(self, @"1");
    block(newName);
}

現(xiàn)在我們使用一下,就可以在block回調(diào)中得到新的屬性值了:

    [self.person CBX_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil block:^(NSString *newName) {
        NSLog(@"%@",newName);
    }];

但是王滤,別以為現(xiàn)在就完了贺嫂,我們只是把新的值從回調(diào)返回,但是并沒有為其賦值雁乡,所以此時(shí)對(duì)象的name屬性的值還是nil第喳。
我們需要在自己寫的Setter函數(shù)內(nèi)為其賦值,但是直接使用Setter方法是不行的(會(huì)循環(huán)調(diào)用)踱稍,這地方要調(diào)用父類的方法才行曲饱,而這個(gè)地方有沒辦法直接調(diào)用父類的方法悠抹,所以我們用消息發(fā)送來(lái)調(diào)用:

void newSetter(id self,SEL _cmd,NSString * newName){
    //調(diào)用父類方法
    struct objc_super mySuper;
    mySuper.receiver = self;
    mySuper.super_class = [self superclass];
    objc_msgSendSuper(&mySuper, @selector(setName:), newName);
    //處理回調(diào)
    void(^block)(NSString *newname) = objc_getAssociatedObject(self, @"1");
    block(newName);
}

大功告成!現(xiàn)在我們就可以在block中處理回調(diào)而不用再實(shí)現(xiàn)回調(diào)方法了扩淀!但是實(shí)際上楔敌,我們只是簡(jiǎn)單的實(shí)現(xiàn)了KVO,我們并沒有對(duì)類型驻谆,方法名等做適配卵凑,這些太復(fù)雜了,這里就不做具體展示了(其實(shí)我也不會(huì))胜臊,Demo我放在了github上勺卢,點(diǎn)擊前往

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末区端,一起剝皮案震驚了整個(gè)濱河市值漫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌织盼,老刑警劉巖杨何,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異沥邻,居然都是意外死亡危虱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門唐全,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)埃跷,“玉大人,你說(shuō)我怎么就攤上這事邮利∶直ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵延届,是天一觀的道長(zhǎng)剪勿。 經(jīng)常有香客問我,道長(zhǎng)方庭,這世上最難降的妖魔是什么厕吉? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮械念,結(jié)果婚禮上头朱,老公的妹妹穿的比我還像新娘。我一直安慰自己龄减,他們只是感情好项钮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般寄纵。 火紅的嫁衣襯著肌膚如雪鳖敷。 梳的紋絲不亂的頭發(fā)上脖苏,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天程拭,我揣著相機(jī)與錄音,去河邊找鬼棍潘。 笑死恃鞋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的亦歉。 我是一名探鬼主播恤浪,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼肴楷!你這毒婦竟也來(lái)了水由?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤赛蔫,失蹤者是張志新(化名)和其女友劉穎砂客,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呵恢,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鞠值,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了渗钉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片彤恶。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鳄橘,靈堂內(nèi)的尸體忽然破棺而出声离,到底是詐尸還是另有隱情,我是刑警寧澤瘫怜,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布术徊,位于F島的核電站,受9級(jí)特大地震影響宝磨,放射性物質(zhì)發(fā)生泄漏弧关。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一唤锉、第九天 我趴在偏房一處隱蔽的房頂上張望世囊。 院中可真熱鬧,春花似錦窿祥、人聲如沸株憾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嗤瞎。三九已至墙歪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贝奇,已是汗流浹背虹菲。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掉瞳,地道東北人毕源。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像陕习,于是被迫代替她去往敵國(guó)和親霎褐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

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

  • 禪與 Objective-C 編程藝術(shù) (Zen and the Art of the Objective-C C...
    GrayLand閱讀 1,624評(píng)論 1 10
  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來(lái)就是把...
    Dove_iOS閱讀 27,139評(píng)論 30 470
  • 基礎(chǔ) 1. 為什么說(shuō)Objective-C是一門動(dòng)態(tài)的語(yǔ)言该镣? 2. 講一下MVC和MVVM冻璃,MVP? 3. 為...
    波妞和醬豆子閱讀 3,321評(píng)論 0 46
  • 起于平损合,終于和— 何為之好 何為之故 故之所指 指之所用 用之無(wú)為 為無(wú)之有 ...
    心亡眼瞎閱讀 249評(píng)論 0 0
  • 文/孟小滿 (一)一覺醒來(lái)省艳,睜開眼,我頓時(shí)迷糊了塌忽。墻壁是白的拍埠,床單是白的,整個(gè)人被禁錮在床上土居≡婀海恍惚中,仿佛看見了一...
    孟小滿閱讀 1,965評(píng)論 65 42