美文网首页iOSiOS面试总结
KVO原理探究+自定义KVO实现

KVO原理探究+自定义KVO实现

作者: it_Xiong | 来源:发表于2019-04-12 16:56 被阅读141次
    • 概念
    • 基本使用
    • 触发模式
    • 属性依赖
    • 容器类的使用
    • 自定义KVO

    概念

    KVO全称Key-Value Observing,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。

    基本使用

    使用KVO分为三个步骤:
    1.通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
    2.在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
    3.当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。
    注册方法

    /**
     注册KVO监听
    
     @param observer 观察者对象
     @param keyPath 需要观察的属性,由于是字符串形式,容易crash,一般利用系统的反射机制 NSStringFromSeletor(seletor(keypath))
     @param options 监听枚举类型
                    OptionNew 接收新值,默认为只接收新值
                    OptionOld 接收旧值
                    OptionInitial在注册时立即接收一次回调,在改变时也会发送通知
                    OptionPrior 改变之前发一次,改变之后发一次
     @param context 传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式,主要用于多个监听器对象监听相同keypath时进行区分
     */
    -(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
    
    

    tips:在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的carsh.
    KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。
    苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。

    监听方法
    观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash。

    /**
     监听回调方法
    
     @param keyPath 监听的属性路径
     @param object 被观察对象
     @param change 监听内容的变化,是个字典
     @param context 一个用来传值的对象,由注册方法添加
     */
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    
    

    触发模式

    自动触发
    控制当前对象的自动调用过程

    /**
     调用模式,是否响应
    
     @param key KVO观察属性
     @return YES 正常发送通知; NO 不发送通知
     */
    +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
    
    

    手动触发

    - (void)willChangeValueForKey:(NSString *)key;
    - (void)didChangeValueForKey:(NSString *)key;
    
    

    属性依赖

    A类的属性是一个拥有多个属性的对象B,要观察该对象,而不想写多个addObserver方法,
    可以在A类的.m文件中重写

    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
       
       NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
       
       //dog属性的相关属性发生变化, 避免因为监听dog的多个属性而写多份KVO监听代码
       if ([key isEqualToString:@"dog"]) {
           keyPaths = [NSSet setWithObjects:@"_dog.age",@"_dog.color",nil];
       }
       return keyPaths;
    }
    
    

    容器类的使用

        //监听容器类
        [self.person addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];
        //该方法无法触发
        [self.person.array  addObject:@"元素"];
        //使用以下方法可以触发,
        NSMutableArray *tempArray = [self.person mutableArrayValueForKey:@"array"];
        [tempArray addObject:@"元素"];
    

    自定义KVO

    KVO是通过isa-swizzling技术实现的
    1.创建一个子类 NSKVONotifying_Person,继承于被观察的类
    2.重写setter方法
    3.外界改变isa指针
    object_getClassName(p)
    开始是原类: isa指向Person
    p addObserver:forKeyPath:options:context:
    注册监听之后:isa指向 NSKVONotifying_Person
    根据这个思路,我自己写了一个自定义KVO,仅供技术交流,不能在项目中直接使用

    
    #import "MyKVOPerson+KVO.h"
    #import <objc/message.h>
    @implementation MyKVOPerson (KVO)
    
    - (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
        
        //创建子类
        //创建一个新的类  模仿系统类 NSKVONotifying_MyKVOPerson
        NSString *oldClassName = NSStringFromClass(self.class);
        NSString *newClassName = [@"MYKVONotifying_" stringByAppendingString:oldClassName];
        
        //创建类 并且注册类
        Class myClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
        objc_registerClassPair(myClass);
        
        //重写setName方法 实际是添加一个和父类同名的方法
        class_addMethod(myClass, @selector(setName:), (IMP)setName, "v@:@");
        
        //外部改变isa指针指向
        object_setClass(self, myClass);
        
        //属性绑定  将观察者保存到当前对象 OBJC_ASSOCIATION_ASSIGN weak 防止循环引用
        objc_setAssociatedObject(self, @"Observer", observer, OBJC_ASSOCIATION_ASSIGN);
    }
    
    void setName(id self,SEL _cmd,NSString *newName) {
      
        NSLog(@"修改成功");
        //调用父类的setName方法,改变name的值
        Class class = [self class];
        object_setClass(self, class_getSuperclass(class));
    //
        ((void(*)(id,SEL,NSString *))objc_msgSend)(self,@selector(setName:),newName);
        
        //拿到观察者
        id observer = objc_getAssociatedObject(self, @"Observer");
        if (observer) {
            //发消息
            ((void (*)(id, SEL,NSString*,id, NSDictionary<NSKeyValueChangeKey,id> *,void *))objc_msgSend)(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"name":newName,@"kind":@1},nil);
        }
        
        //修改为self,改回子类
        object_setClass(self, class);
        
    }
    
    

    详细Dmeo请移步GitHub👉KVODemo

    相关文章

      网友评论

        本文标题:KVO原理探究+自定义KVO实现

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