前面我們在使用Block的時候提到了函數式編程和鏈式調用的用法,但是實際上Block還有一種編程思想,就是響應式編程。
函數式編程是把相關邏輯代碼寫到一起,鏈式調用是可以使用點語法不停的調用方法,而響應式編程則是把事件回調邏輯和使用寫到一起,著名的RAC框架就是響應式編程的代表。
不過不難看出,Block的靈活使用,簡化了我們代碼的復雜度,提升了我們編寫程序的效率。
今天,我們就利用之前所學過的Block和Runtime做一個有趣的事情,就是自己寫一個KVO,并且用函數式響應式編程思想進行一個改造。
-
什么是KVO
KVO即(Key - Value - Observer),是觀察者模式的一種體現,它可以觀察對象的一個屬性,當它發生改變的時候,觸發回調事件,方便我們進行邏輯操作。
我們先使用一下KVO,然后再對其進行分析。這里,我們自己創建一個類Person
,給它一個屬性name
,并且對它的name
屬性進行觀察。為了方便測試,我們添加了一個按鈕用來修改它的名字。
KVO的使用步驟:
- 給對象添加觀察者
//初始化對象
self.person = [Person new];
//添加觀察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
- 實現回調方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",[change objectForKey:NSKeyValueChangeNewKey]);
}
}
- 觸發回調事件
- (IBAction)changeName:(id)sender {
NSString *defaultName = @"張三";
self.person.name = [defaultName stringByAppendingFormat:@"%d",i++];
}
- 移除觀察者
- (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上,點擊前往。