美文网首页
利用 runtime & block 方式实现 KVO。

利用 runtime & block 方式实现 KVO。

作者: 人话博客 | 来源:发表于2018-04-01 18:26 被阅读0次

    上一篇博客中,说明了 KVO 的执行过程和基本的实现原理。

    KVO的执行原理

    1. 对象本身作为事件的发布者,在自己被观察者(通常是那个包含自己的控制器)观察的属性发生改变的时候,向观察者发布属性修改了的通知。(本质就是在 setter 方法调用的时候执行发布)
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
    
    1. 控制器,本身之前实现订阅好这个 setter 通知,并在事件响应函数中,对这个的事件发布做出相应。
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        
        NSLog(@"%@",change);
    }
    

    这些道理都懂,但如何自己在了解了 KVO 实现原理之后,自己实现一个 KVO 呢?

    为什么要自己实现 KVO?

    1. 熟悉 runtime 的各种使用方法。
    2. 传统的 KVO,所有的属性订阅都几种在了一个方法相应体里面。看起来不太优雅(装逼)
      if ([object isKindOfClass:[xxx class]] && [keyPath isEqualToString:@"xxx"]) {
            
        } else if ([object isKindOfClass:[xxx class]] && [keyPath isEqualToString:@"xxx"]) {
            
        } else if ([object isKindOfClass:[xxx class]] && [keyPath isEqualToString:@"xxx"]) {
            
        } ......
    

    实现目标

    希望能够以 block 回调的方式来自己实现 KVO。
    好处在于,每一个属性对应一个自己的回调 block。
    这样代码看起来比较清晰和紧凑。
    也符合函数是编程的思想。

    最终需要实现的代码效果

      [self.person rl_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil withBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
            NSLog(@"observer : %@ keyPath : %@ oldValue : %@ newValue : %@",observer,keyPath,oldValue,newValue);
        }];
        
        [self.person rl_addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew) context:nil withBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
           //... 这是 age 的改变时的 KVO block 回调
        }];
        
        .....
    

    在开始先自己的目标方法之前,首先在回顾一下 KVO 的实现基本步骤

    1. 创建一个当前类的子类。
    2. 在子类中重写当前属性的 setter 方法。
    3. 在子类重写的 setter 方法里面,手动的调用 KVO 实现的两句代码。
    image.png

    当然,和真实的 KVO 相比,这里缺少了 _isKVO , class , dealloc 的实现。

    首要目的,先是以函数式编程的方法,来实现自己的 KVO。


    开始动手来实现自己的 KVO。

    第一步:添加一个 NSObject 的分类

    typedef void(^KVOBlock)(id observer,NSString *keyPath,id oldValue ,id newValue);
    
    @interface NSObject (RLKVO)
    
    - (void)rl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context withBlock:(KVOBlock)block;
    
    @end
    

    方法名哪来的?
    复制粘贴系统自带的 KVO 的方法。
    后面加入了一个 block用来当属性发生改变时,回调观察者。

    第二步:实现这个方法

    先考虑一下,在这个方法内部,需要做什么事情。

    1. keyPath 的有效性检查。
    2. 根据当前 self,创建一个子类。
    3. 当类创建完毕之后,修改当前 self 的 isa 指针到这个子类。
    4. 在子类中,添加一个 set方法,用来重写父类的 setter 方法。(用 class_addMethod 添加方法 sel 和 真实的函数名,不用匹配,只要建立双方的联系就行了。此例子中,sel = setName: 而真实的 C 函数叫 kvoSetter)。sel & IMP 不需要匹配名字
    5. 在子类的 setter 方法内部,调用 self willChangeValueForKey: & self didChangeValueForkey:

    当然,还有有一些细枝末节的东西。在代码注释里会有说明。

    - (void)rl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context withBlock:(KVOBlock)block {
        // keyPath 的检查。
        NSString *setterName = setterFormat(keyPath);
        SEL sel = NSSelectorFromString(setterName);
        // 这里获取 method 的意义在于,可以方便的获取当前方法的 EncodingType 一遍在添加方法class_addMethod的时候,可以设置 EncodingType.
        Method setterMethod = class_getInstanceMethod([self class], sel);
        if (!setterMethod) {
            @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%@ have not %@ method",[self class],setterName] userInfo:nil];
        }
        // 获取前类的类名,并作为之后创建子类的父类。
        NSString *superClassName = NSStringFromClass([self class]);
        const char *type = method_getTypeEncoding(setterMethod); // 通过方法结果提,拿到方法编码。EncodingType。否则就需要自己手拼写  v@:@ 麻烦。
        
        // 动态创建类
        Class newClass = [self createClassFromSuperName:superClassName sel:sel encodingType:type];
        
        // 替换当前 self 的 isa 指针
        object_setClass(self, newClass);
        
        // 保存信息
        RLKVO_Info *info = [[RLKVO_Info alloc] initWithObserver:observer block:block keypath:keyPath];
        
        NSMutableArray *infoArray = objc_getAssociatedObject(self, &infoArrayPro);
        if (!infoArray) {
            infoArray = [NSMutableArray array];
            objc_setAssociatedObject(self, &infoArrayPro, infoArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        
        [infoArray addObject:info];
        
        
    }
    
    

    此段代码解释:

    1. 首先根据用户传入的 keyPath 字符串,配置成 setKeyPath: 字符串,表示 setter 方法的sel。
    2. 获取这个 setKeyPath: 的 sel
    3. 根据 sel 获取当期对应的方法 Method
    4. 如果 Method 不存在,说明传入的 keyPath 是错误的。对象没有这个属性,抛出异常。
    5. 获取当前对象的 className,以备创建子类的时候使用。
    6. 根据 Method 拿到方法的 EncodingType 签名。
    7. 动态的创建子类。

    创建当前类的子类方法

    /**
     根据父类创建子类
     
     @param superName 父类类型字符串
     @param sel 父类当前方法的 sel
     @param encodingType  父类当前方法的 EncodingType
     @return  返回以当前父类创建的新类。
     */
    - (Class)createClassFromSuperName:(NSString *)superName sel:(SEL)sel encodingType:(const char *)encodingType {
        
        // 创建一个类.
        Class newClass = objc_allocateClassPair(
                                                NSClassFromString(superName), // 当前类的基类
                                                [NSString stringWithFormat:@"RLKVO_%@",
                                                 NSStringFromClass([self class])].UTF8String,// 类名
                                                0);
        
        
        // const char *types = method_getTypeEncoding(class_getInstanceMethod(NSClassFromString(superName), @selector(class)));
        
        // 往新类中添加方法
        class_addMethod(newClass, sel, (IMP)kvoSetter, encodingType);
        
        
        // 类创建完毕之后,注册到 runtime
        objc_registerClassPair(newClass);
        
        
        // 返回这个新类。
        return newClass;
    }
    
    
    

    此段代码说明:

    1. 动态的创建一个子类,类名为 RLKVO_父类的名字
    2. 往新类中,添加一个 kvoSetter 的方法,方法签名使用父类的 setKeyPath: 。
    3. 新类创建完毕,还并没有完成,需要把新类注册到 runtime。
    4. 最后返回已经注入到 runtime 的这个子类。

    当子类创建,完毕之后,就需要把当前对象的 isa 指针改成新创建的这个类了。
    也就是第一段代码的。

    // 动态创建类
        Class newClass = [self createClassFromSuperName:superClassName sel:sel encodingType:type];
        
        // 替换当前 self 的 isa 指针
        object_setClass(self, newClass);
    

    到目前为止,做的事情,主要包括创建了一个基于当前类的子类 & 把当前类的 isa 指针改成了子类

    往子类中添加的 kvoSetter 方法具体实现

    // 使用 class_addMethod runtime 添加方法。
    void kvoSetter(id self,SEL _cmd,id newValue) {
        // 拿到 setter
        NSString *setterName = NSStringFromSelector(_cmd);
        // 根据 setter 拿到 getter
        NSString *getterName = getterFormat(setterName);
        
        id oldValue = [self valueForKey:getterName];
        
        if (!getterName) {
            @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"getter 方法不存在" userInfo: nil];
        }
        
        // 手动开启 KVO
        [self willChangeValueForKey:getterName];
        // 调用父类的方法。
        // 定义一个函数指针
        void(*objc_msgSendRLKVO)(void *,SEL ,id) = (void *)objc_msgSendSuper;
        
        struct objc_super superClassStruct = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self))
        };
        
        objc_msgSendRLKVO(&superClassStruct,_cmd,newValue);
        
        [self didChangeValueForKey:getterName];
        
        
        //
        
        NSMutableArray *infoArrM = objc_getAssociatedObject(self, &infoArrayPro);
        if (infoArrM) {
            [infoArrM enumerateObjectsUsingBlock:^(RLKVO_Info *info, NSUInteger idx, BOOL * _Nonnull stop) {
                info.block(info.observer, info.keyPath, oldValue, newValue);
            }];
        }
    }
    
    

    在方法实现中,最重要的就是需要调用 block,把 KVO 的相关数据回调出去给观察者。

     NSMutableArray *infoArrM = objc_getAssociatedObject(self, &infoArrayPro);
        if (infoArrM) {
            [infoArrM enumerateObjectsUsingBlock:^(RLKVO_Info *info, NSUInteger idx, BOOL * _Nonnull stop) {
                info.block(info.observer, info.keyPath, oldValue, newValue);
            }];
        }
    

    最后运行结果:

    - (void)viewDidLoad {
        [super viewDidLoad];
        _person = [RLPerson new];
        _person.name = @"李四";
        [_person rl_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil withBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
            NSLog(@"keyPath : %@ oldValue: %@ newValue : %@",keyPath,oldValue,newValue);
        }];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        _person.name = [NSString stringWithFormat:@"%@-",_person.name];
    }
    
    KVO+BLOCK 实现2.gif

    此次实现过程中,代码细节有很多,光使用文字很难说的清楚。
    我把源码放到了 github 上面。有兴趣的小伙伴可以下载来看看。

    注意:此 demo 应该不能使用到实际的开发环境中。还有很多细节没有处理。比如 _iskVO class dealloc 等。仅供学习之用。

    相关文章

      网友评论

          本文标题:利用 runtime & block 方式实现 KVO。

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