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<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观察类并不会销毁。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342