利用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阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容