前面我們?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的使用步驟:
- 給對(duì)象添加觀察者
//初始化對(duì)象
self.person = [Person new];
//添加觀察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
- 實(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]);
}
}
- 觸發(fā)回調(diào)事件
- (IBAction)changeName:(id)sender {
NSString *defaultName = @"張三";
self.person.name = [defaultName stringByAppendingFormat:@"%d",i++];
}
- 移除觀察者
- (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)擊前往。