KVC、KVO从使用到原理实现

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

本文从使用到底层实现介绍这两个概念
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 _set setIs顺序找这几个方法,找到就赋值,取值的时候也有相关逻辑。
最主要的原因就是,我们自己写代码或者编译器生成代码的时候,会添加一些特殊符号(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方法赋值,直接找 成员变量 _ 、_is 、is按照顺序,如果找到,直接赋值,找不到,报UnknownKeyException错误

get取值过程

1.判断key的合法性

2.找到相关方法 get 、countOf、  objectInAtIndex

3.判断类方法accessInstanceVariablesDirectly

4.寻找ivar的成员列表_ 、_is 、is

返回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 *)change context:(void *)context{
    NSLog(@"%@",change);
}

//移除观察,很重要,如果不移除,经常发生一些不好排查的问题,这个操作也会将isa指针指回原对象。
 [ self  removeObserver:self forKeyPath:@""];

//这个函数可以设置有依赖的观察,也就是当其他属性变化,影响我们观察属性的时候,可已经这些属性都放到集合里
+ (NSSet *)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从使用到原理实现)