美文网首页
KVC、KVO从使用到原理实现

KVC、KVO从使用到原理实现

作者: RainyHand | 来源:发表于2020-05-08 14:03 被阅读0次

    原创总结性文章,有疑问及时联系,谢谢

    本文从使用到底层实现介绍这两个概念
    KVC:键值编码,通过key来访问和操作某个属性,常用的API有以下四个

    -(void)setValue:(id)value forKey:(NSString *)key
    -(void)setValue:(id)value forKeyPath:(NSString *)keyPath
    - (id)valueForKey:(NSString *)key
    -(id)valueForKeyPath:(NSString *)keyPath
    
    一些特殊使用
    1.keyPath层级调用,如果对象中包含其他对象,直接赋值其他对象的时候可以使用,取值相同。
    [person setValue:@"测试" forKeyPath:@"student.subject"];
    
    2.字典转模型
     [model setValuesForKeysWithDictionary:dict];
    注意:此处赋值要考虑空值和key没有的情况。
    
    3.聚合操作符
     float avg = [[personArray valueForKeyPath:@"@avg.height"] floatValue];
    count
    sum
    max
    min
    数组中包含对象,通过keyPath,直接找到height属性,并且进行数据运算
    一般用不到...
    
    4.其他 @distinctUnionOfObjects @unionOfObjects
    
    
    原理理解:

    从开始的定义我们也看出,KVC就是通过字符串去设置或者取出某个对象的属性或者是ivar,只不过底层实现的时候,加了一些判断,赋值的时候,找set<Key> _set<Key> setIs<Key>顺序找这几个方法,找到就赋值,取值的时候也有相关逻辑。
    最主要的原因就是,我们自己写代码或者编译器生成代码的时候,会添加一些特殊符号(eg:property属性,系统默认生成_ivar 和 相应的set 和 get方法),所以在取值或者赋值的时候,将特殊的变量都考虑到。

    以下是详细的set过程

    1.set值的时候,首先系统会生成以下三个字符串,判断有没有字符串对应的方法,如果有,自己调用赋值,并且return返回
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    2.accessInstanceVariablesDirectly  调用这个类方法,判断返回值,默认是true,直接向下走,如果是false,报错停止
    3.走到这,已经没有相应的set方法赋值,直接找 成员变量 _<key> 、_is<Key> 、<key> 、is<Key>按照顺序,如果找到,直接赋值,找不到,报UnknownKeyException错误
    

    get取值过程

    1.判断key的合法性
    
    2.找到相关方法 get<Key>、 <key> 、countOf<Key>、  objectIn<Key>AtIndex
    
    3.判断类方法accessInstanceVariablesDirectly
    
    4.寻找ivar的成员列表_<key> 、_is<Key>、 <key> 、is<Key>
    
    返回nil
    

    KVC的赋值在没有set方法的时候,是直接赋值的,但是我们通过KVO能监控到吗,这涉及到了KVO的底层实现原理,可以监控到,在直接给ivar赋值的时候,KVC底层是手动实现调用通知函数的

    void _DSSetValueAndNotifyForKeyInIvar(id object, SEL selector, id value, NSString *key, Ivar ivar, IMP imp) {
        [object d_willChangeValueForKey:key];
        
        ((void (*)(id,SEL,id,NSString *, Ivar))imp)(object,NULL,value,key,ivar);
        
        [object d_didChangeValueForKey:key];
    }
    可以看到,在设置ivar的时候,是调用了will   和  did这两个函数的
    和KVO实现是一样的。
    

    注意:

    1.访问非对象类型,要将value转换成NSValue类型。
    2.字典转模型的时候,注意设置空值检测和空的key检测,写以下 两个函数
    1. 在使用KVC赋值的时候,防止没有相关属性,可以在类中写
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
        一个空的方法,防止崩溃
    }
    
    2.设置nil的时候会崩溃
    - (void)setNilValueForKey:(NSString *)key{
    }
    拦截控制,不让崩溃,
    注:不过这个方法,很多类型的key进不来只有 number 和 NSvalue能进来
    

    KVO键值观察

    监控某个对象的属性,如果属性值变化了,就会回调observer的函数。
    使用:

     [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    参数详解:
    - KeyPath:就是监测的属性值
    - options:         
    NSKeyValueObservingOptionNew:提供更改前的值
    NSKeyValueObservingOptionOld:提供更改后的值
    NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
    NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(即一次修改有两次触发)
    -  context: 是一个void * 指针,根据官网提示,可以根据这个值判断不同的通知,主要是区分不同的对象,观察相同的属性的时候
    
    
    //接到改变的回调函数
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"%@",change);
    }
    
    //移除观察,很重要,如果不移除,经常发生一些不好排查的问题,这个操作也会将isa指针指回原对象。
     [ self  removeObserver:self forKeyPath:@""];
    
    //这个函数可以设置有依赖的观察,也就是当其他属性变化,影响我们观察属性的时候,可已经这些属性都放到集合里
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
       }
    
    // 自动开关 ,是否允许这个对象接受KVO的观察的开关,关闭以后我们可以自己发送调用的通知函数
    + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
        return YES;
    }
    

    以上就是经常使用的API,下边过一下原理相关

    我们都知道KVO底层实现是通过runtime动态实现一个继承于被观察对象的子类,为什么呢?
    我们要实现观察,就要做到两件事;1.在值改变的时候要通知我。2.还不能影响之前的赋值过程。
    起始只要能实现以上两点,采用其他方案也是可以的,系统的实现方案,采用了高度封装,可以理解就是不希望使用者了解底层的实现。
    基本流程

    • 判断被观察者有没有实现set方法,false直接返回
    • 动态生成一个子类,继承于被观察对象的类,将ISA指向这个类
    • 在被观察类中重写set方法
    • set方法中,调用[super set:]方法赋值
      调用 [self willChangeValueForKey:@""];
      [self didChangeValueForKey:@""];
      这两个函数通知observer的回调函数(通过探究源码得知,真正调用oberver的是didChangeValueForKey函数)
    • 重写set方法的时候,还重写了其他几个函数,包括:
    伪代码
    -(void)setAge:(int)age{
        _NSSetIntValueAndNotify()  /‘/这是个C函数
    }
    void _NSSetIntValueAndNotify(){
        [self willChangeValueForKey: @“age”]
        [super setAge:age]
        [self didChangeValueForKey: @“age"]
    }
    
    -(void)didChangeValueForKey{
        [observer observerValueForKeyPath:key ofObject:self change:nil context:nil];
    }
    //额外生成的方法
    -(Class)class{
       //关键
        return class_getSuperclass(object_getClass(self));
         在这返回的是被继承类的 类对象
          原因就是开发的时候没必要暴露出运行时产生的这个类,屏蔽了内部实现。
    }
    -(void)dealloc{
        //收尾工作
          将isa重新指向父类
    }
    -(BOOL)isKVO{
        return YES;
    }
    

    根据以上的步骤,我们可以自定义实现一个KVO,这样我们就可以使用函数是编程的思想,引入block,不用使用回调函数。

    注意: 我们在remove掉观察者的时候,通过打印类的列表发现,创建的KVO观察类并不会销毁。

    相关文章

      网友评论

          本文标题:KVC、KVO从使用到原理实现

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