原创总结性文章,有疑问及时联系,谢谢
本文从使用到底层实现介绍这两个概念
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观察类并不会销毁。