美文网首页selector
利用Runtime和函数响应式编程自己实现OC的KVO

利用Runtime和函数响应式编程自己实现OC的KVO

作者: 楚槟夕 | 来源:发表于2017-11-03 20:01 被阅读112次

    前面我们在使用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上,点击前往

    相关文章

      网友评论

        本文标题:利用Runtime和函数响应式编程自己实现OC的KVO

        本文链接:https://www.haomeiwen.com/subject/dxqnpxtx.html