KVO

作者: 韡韡_ea9b | 来源:发表于2020-02-02 16:09 被阅读0次

    通知和代理:
    通知:一对多(随处可发通知,随处可以接收通知)
    优点:发送者和接受者都不需要知道对方是谁
    缺点:发送方没有办法接受到反馈值

    代理:一对一(一触发通知,即刻接收)
    优点:支持的类有详尽的信息
    缺点:必须支持委托

    KVO

    概述

    Key-Value Observing,键值观察,观察者模式的衍生
    对目标对象的某属性添加观察,当属性发生变化时,通过触发观察者对象实现的接口方法,自动通知观察者
    较完美的将目标对象和观察者对象进行解耦

    KVO的定义是对NSObject的扩展所实现,所以,继承自NSObject的类,都能使用KVO

    过程

    1. 注册观察者
    2. 监听回调
    3. 移出监听
    • 注册
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
    observer:观察者
    keyPath:所观察的属性
    options:属性配置
    context:上下文
    

    options

    NSKeyValueObservingOptionNew:change字典包括改变后的值
    NSKeyValueObservingOptionOld:change字典包括改变前的值
    NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
    NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)
    
    • 回调
    1. 调用方法
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
    
    1. 自动
    //通过属性的点语法间接调用
    objc.name = @"";
    
    // 直接调用set方法
    [objc setName:@"Savings"];
     
    // 使用KVC的setValue:forKey:方法
    [objc setValue:@"Savings" forKey:@"name"];
     
    // 使用KVC的setValue:forKeyPath:方法
    [objc setValue:@"Savings" forKeyPath:@"account.name"];
    
    1. 手动
    第一步.重写方法,YES可以调用,NO不可以
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
        BOOL automatic = NO;
        if ([theKey isEqualToString:@"name"]) {
            automatic = NO;//对该key禁用系统“自动通知”,
            
            //若要直接禁用该类的KVO则直接返回NO;
        }
        //对其他非手动实现的key,转交super处理
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
        return automatic;
    }
    
    第二步.重写setter方法
    - (void)setName:(NSString *)name {
        if (name != _name) {
            [self willChangeValueForKey:@"name"];
            _name = name;
            [self didChangeValueForKey:@"name"];
            //在操作前后分别调用will和did方法
            //用于通知系统该key的属性值即将和已经发生变化
        }
    }
    
    • 移除
    - (void)dealloc{
        [self removeObserver:self forKeyPath:@"age"];
    }
    

    崩溃原因:
    1.观察者未实现监听
    2.未及时移除见监听
    3.多次移除监听

    键值观察依赖键:
    一个属性的值依赖另一个对象中的一个多个属性。
    即一个属性发生变更,被依赖的属性值,也应当为其变更进行标记

    #import "TargetWrapper.h"
    
    - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object  change:(NSDictionary *)change context:(void *)context{
        if ([keyPath isEqualToString:@"age"]){
            Class classInfo = (Class)context;
            NSString * className = [NSString stringWithCString:object_getClassName(classInfo) encoding:NSUTF8StringEncoding];
            NSLog(@" >> class: %@, Age changed", className);
            NSLog(@" old age is %@", [change objectForKey:@"old"]);
           NSLog(@" new age is %@", [change objectForKey:@"new"]);
        }else if ([keyPath isEqualToString:@"information"]){
            Class classInfo = (Class)context;
            NSString * className = [NSString stringWithCString:object_getClassName(classInfo) encoding:NSUTF8StringEncoding];
            NSLog(@" >> class: %@, Information changed", className);
            NSLog(@" old information is %@", [change objectForKey:@"old"]);
            NSLog(@" new information is %@", [change objectForKey:@"new"]);
        }else{
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }
    

    与RunLoop之间的关系

    与线程之间的关系:
    KVO行为是“同步”的,发生与所观察的值发生变化的同样的线程上。
    没有RunLoop的处理
    如果使用KVO来改变其他线程的属性值,应格外小心,除非能确定所有的观察者都使用线程安全的方法
    所以,使用多个队列和线程,不应该在它们之间使用KVO

    中间类

    实现原理:
    中间类!!:是原类的子类,并将当前对象的isa指针指向这个中间类
    添加KVO之后,person的isa指向 NSKVONotifying_Person 类对象
    setAge的实现也进行类改变,调用的是Foundation 中 _NSSetLongLongValueAndNotify 方法

    中间类的命名规则为:NSKVONotifying_xxx


    image

    中间类的内部实现:

    - (void)setAge:(int)age{
        _NSSetLongLongValueAndNotify();//这个方法调用顺序,何处调用,都在setter方法改变中详解
    }
    
    重写class方法
    如果没有重写此方法,则当对象调用class方法时,
    会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法
    因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_Person
    就会将该类暴露出来,也给开发者造成困扰
    - (Class)class {
        return [LDPerson class];
    }
    
    - (void)dealloc {
        // 收尾工作
    }
    
    这个方法可以当做使用了KVO的一个标记,系统可能也是这么用的。
    如果我们想判断当前类是否是KVO动态生成的类,就可以从方法列表中搜索这个方法
    - (BOOL)_isKVOA {
        return YES;
    }
    

    setter方法详解:
    利用父类person分析

    - (void)setAge:(int)age{
        _age = age;
        NSLog(@"setAge:");
    }
    
    - (void)willChangeValueForKey:(NSString *)key{
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey");
    }
    
    - (void)didChangeValueForKey:(NSString *)key{
        NSLog(@"didChangeValueForKey - begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey - end");
    }
    @end
      
    //打印结果
    KVO-test[1457:637227] willChangeValueForKey
    KVO-test[1457:637227] setAge:
    KVO-test[1457:637227] didChangeValueForKey - begin
    KVO-test[1457:637227] didChangeValueForKey - end
    KVO-test[1457:637227] willChangeValueForKey
    KVO-test[1457:637227] didChangeValueForKey - begin
    KVO-test[1457:637227] didChangeValueForKey - end
    

    首先调用willChangeValueForKey:方法
    然后调用setAge:方法
    最后调用didChangeValueForKey:方法
    其中,调用[super didChangeValueForKey:key];通知属性值已经改变,然后监听自己的方法

    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    

    这里记一个小点:

    NSLog(@"%@",[objA class]);//打印:ObjectA
    NSLog(@"%@",object_getClass(objA));//打印:NSKVONotifying_ObjectA(返回isa的指向)
    

    相关文章

      网友评论

          本文标题:KVO

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