美文网首页iOS开发
KVO的使用及底层探究

KVO的使用及底层探究

作者: 姜益达 | 来源:发表于2019-06-22 17:20 被阅读0次

    KVO的使用

    KVO使用起来非常简单,三个步骤就搞定啦

    1、通过addObserver: forKeyPath: options: context方法注册成为观察者,这样就可以观察到keyPath属性变化事件
    2、实现observeValueForKeyPath: ofObject: change:context:方法,当属性值发生变化,KVO会回调这个方法通知观察者
    3、当不需要监听的时候调用removeObserver: forKeyPath将KVO移除

    KVO的触发模式

    KVO有两种触发模式,手动和自动(不设置默认为自动)
    来看看手动怎么触发

    //在观察者的类中实现下面这个方法
    // 1、模式调整 返回YES为自动 NO为手动
    +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
        if ([key isEqualToString:@"name"]) {
            return NO;
        }
        return YES;
    }
    //2、手动触发KVO
     [_p willChangeValueForKey:@"name"];
     _p.name = [NSString stringWithFormat:@"%d",a++];
     [_p didChangeValueForKey:@"name"];
    

    手动触发的好处就是我们可以根据需求的不同来决定要不要触发KVO

    KVO观察对象属性

    假设我们有一个Dog类,类里面有age、level两个属性,那么我们怎么使用KVO观察对象呢?

    //在观察者的类中实现下面这个类方法
    +(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
        NSSet* keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"dog"]) {
            keyPaths = [[NSSet alloc]initWithObjects:@"_dog.age",@"_dog.level", nil];
        }
        return keyPaths;
    }
    
    //注册观察者 观察dog属性
    [_p addObserver:self forKeyPath:@"dog" options:NSKeyValueObservingOptionNew context:nil];
    

    简直是 so easy 有木有啊!

    不过,作为一名程序员,就要有打破砂锅问到底的精神,KVO究竟是怎么实现的呢?

    下面一起来揭开KVO的神秘面纱,let's go

    KVO原理探究

    KVO的内部实现分以下三个步骤

    1、创建一个子类(为什么KVO的实现使用继承而不使用分类?因为KVO会重写set方法,而使用分类重写set方法会覆盖掉原来类的set方法)
    2、重写set方法
    3、外界改变isa指针

    -(void)JYC_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
        //1、创建一个类 父类为self即调用者
        NSString* oldClassName = NSStringFromClass([self class]);
        NSString* newClassName = [@"JYCkvo_" stringByAppendingString:oldClassName];
        Class myClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
        objc_registerClassPair(myClass);//注册类
        
        //2. 重写set方法(实际上添加了set方法,子类中没有没有父类的方法,仅仅是可以调用) myclass
        class_addMethod(myClass, @selector(setName:), (IMP)setName, "V@:@");
        
        //3、修改isa指针(将调用者指向子类),这样调用者调用set方法会来到本类中重写的set方法中
        object_setClass(self, myClass);
        
        //4、将观察者保存到当前对象  OBJC_ASSOCIATION_ASSIGN 属性类型是weak,防止循环引用
        objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN);
    }
    
    void setName(id self, SEL _cmd ,NSString* newname){
        NSLog(@"---%@",newname);
        //    调用父类的setName:方法
        Class class = [self class];
        object_setClass(self, class_getSuperclass(class));//当前类改为父类
        objc_msgSend(self, @selector(setName:),newname);
        
        //    拿到观察者
        id observer = objc_getAssociatedObject(self, "observer");
        if(observer){
            objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"new:":newname,@"kind:":@1},nil);
        }
        
        //改回子类
        object_setClass(self, class);
        
    }
    
    ⚠️任何OC方法的调用本质上就是消息发送msgsend,发送的时候会包含两个隐式参数,分别是调用者和方法编号。

    相关文章

      网友评论

        本文标题:KVO的使用及底层探究

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