利用Runtime和函數響應式編程自己實現OC的KVO

前面我們在使用Block的時候提到了函數式編程和鏈式調用的用法,但是實際上Block還有一種編程思想,就是響應式編程。
函數式編程是把相關邏輯代碼寫到一起,鏈式調用是可以使用點語法不停的調用方法,而響應式編程則是把事件回調邏輯和使用寫到一起,著名的RAC框架就是響應式編程的代表。
不過不難看出,Block的靈活使用,簡化了我們代碼的復雜度,提升了我們編寫程序的效率。
今天,我們就利用之前所學過的Block和Runtime做一個有趣的事情,就是自己寫一個KVO,并且用函數式響應式編程思想進行一個改造。

  • 什么是KVO

KVO即(Key - Value - Observer),是觀察者模式的一種體現,它可以觀察對象的一個屬性,當它發生改變的時候,觸發回調事件,方便我們進行邏輯操作。
我們先使用一下KVO,然后再對其進行分析。這里,我們自己創建一個類Person,給它一個屬性name,并且對它的name屬性進行觀察。為了方便測試,我們添加了一個按鈕用來修改它的名字。

KVO的使用步驟:

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

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

特別需要注意的一點是,KVO只有在調用Setter方法的時候,才會進行回調,直接使用下劃線修改是不會觸發KVO回調的。
接著,我們再來分析一下KVO的實現原理。KVO其實是用Runtime實現的,怎么證明呢?我們在添加觀察者那一行打一個斷點,單步執行后,再看我們的self.person的類型:


可以看到,person變量的isa指針,指向的類型變成了NSKVONotifying_Person,這說明KVO在運行時改變了所觀察對象的類型,這個類是Person類的子類。我們可以通過這句代碼打印出它的父類來證明:

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

前面我們知道只有Setter方法被調用才可以進入回調,那說明,這個子類一定重寫了父類屬性的Setter方法,并且在其中做了監聽以及事件的處理。

明白了這些,我們就可以自己利用Runtime去編寫一個KVO了。

首先,KVO好像誰都可以直接調用,不需要導入頭文件,我們因此可以推測,它應該是NSObject的一個Category,我們也順著這個思路,創建一個NSObject的分類,我們這里命名為CBX_KVO,并且用一個相同的方法(名字我們加一個前綴)來添加觀察者:

@interface NSObject (CBX_KVO)

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

@end

接下來,我們需要在方法的實現里面做一些事情了,根據我們前面的分析,我們要動態創建一個新類,這個類是觀察對象的子類,并且重寫它對應屬性的Setter方法:

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

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

寫到這里,我們可以先測試一下,添加我們自己的觀察者,每次點擊按鈕改變name屬性的時候能不能打印出新的值:

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


但是實際上,打印出來的并不是我們想要的改變以后的name值,而是這個對象自己,這是因為OC方法都有兩個隱藏的參數:self_cmd,我們自己寫的函數并沒有添加這兩個參數,所以讀出來的默認是第一個參數。
接下來,我們改變一下函數:

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

再測試一下:


現在可以順利打印出值了。然后我們就可以在自己寫的函數里利用Block回調來簡化我們的KVO。
首先,我們需要在前面的CBX_addObserver方法添加一個Block參數來回調處理后續邏輯。
然后在修改了屬性以后,在函數內部調用Block,就可以實現回調的效果了。
但是怎么在我們寫的函數里面獲取Block呢?可以有兩種方法,第一種是把block當做參數傳下去,另外一種就是把block作為屬性添加給新類,然后函數內部可以通過self獲取并調用。我們這里使用第二種:

- (void)CBX_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block:(void (^)(NSString *newName))block{
    
    //創建一個觀察對象的子類
    NSString *oldName = NSStringFromClass(self.class);
    NSString *newName = [@"CBX_KVO_" stringByAppendingString:oldName];
    Class NewClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    //注冊新類
    objc_registerClassPair(NewClass);
    //改變self的類型
    object_setClass(self, NewClass);
    //為其添加一個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){
    //處理回調
    void(^block)(NSString *newname) = objc_getAssociatedObject(self, @"1");
    block(newName);
}

現在我們使用一下,就可以在block回調中得到新的屬性值了:

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

但是,別以為現在就完了,我們只是把新的值從回調返回,但是并沒有為其賦值,所以此時對象的name屬性的值還是nil
我們需要在自己寫的Setter函數內為其賦值,但是直接使用Setter方法是不行的(會循環調用),這地方要調用父類的方法才行,而這個地方有沒辦法直接調用父類的方法,所以我們用消息發送來調用:

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

大功告成!現在我們就可以在block中處理回調而不用再實現回調方法了!但是實際上,我們只是簡單的實現了KVO,我們并沒有對類型,方法名等做適配,這些太復雜了,這里就不做具體展示了(其實我也不會),Demo我放在了github上,點擊前往

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容