KVO/KVC
KVO
key-value-observer 观察者模式
通俗来解释就是说,我们监听某个对象的属性,如果发生了变化,则由该对象通知观察者
这几篇文章可以一起食用:
https://www.jianshu.com/p/d0032fd9397b
https://www.jianshu.com/p/5e85e9daaa44
https://www.jianshu.com/p/badf5cac0130
使用教程
1.注册对象
在我们想要监听的对象中调用方法:
[boss addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@"123"];
- 只要是NSObject,都会有这个方法,尽管调用就好了
- addObserver:由谁来监听这个对象,这个地方是self,那么这个类就应该实现稍后要说的一个方法
- forKeyPath: 监听的属性名,给一个这个对象监听的属性名字,没有对应的会崩溃
- options: 参数的选项,这个地方现在不理解没关系,现在的参数设置就是说监听者会记录原来的值和改变后的值
- context:一个携带信息,如果一个对象的多个属性被监听,可以用这个来区分
2.实现回调函数
当我们监听的对象发生变化之后,会调用一个函数
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
![](https://img.haomeiwen.com/i13754622/e917880e4caa6afb.png)
下面是测试:
![](https://img.haomeiwen.com/i13754622/8f6d03e06eadd52e.png)
从测试中我们就能看出,change这个字典中保存的是什么键值对,这和前面的option
参数设置有关,我设置了old
和new
,意味着change这个字典会保存新值和旧值
KVO原理
简单的来概括原理其实就是,被监听的对象有了一个新的类,这个类中重写了setter方法并且会代替原来的类,而这个setter方法就是调用KVO机制的触发器,当属性被改变时,setter方法就会调用KVO来通知观察者
专业点来讲,KVO使用了isa-swizzling
机制
- isa: 每个实例对象都有一个指针来指向自己的原生类,叫做isa
简单来分析过程就是:
1.系统会为被监听的对象会生成一个新的类,这个类的名字以NSNotifying_
为前缀
2.这个类重写了setter和getter方法,至于怎么样写的,等会我们有模拟
3.被监听的对象的isa会指向这个新生成的类
这张图很经典
![](https://img.haomeiwen.com/i13754622/4017e697ae3d8213.png)
下面是验证:
![](https://img.haomeiwen.com/i13754622/2bfa687828066a5e.png)
![](https://img.haomeiwen.com/i13754622/61b1cc726207ddf6.png)
手动KVO
既然我们知道KVO的原理是重写了setter方法,那么我们可以试着模拟一下,下面两个方法是很重要
[self willChangeValueForKey:@"name"];
[self didChangeValueForKey:@"name"];
![](https://img.haomeiwen.com/i13754622/008ac94d80ac7b94.png)
下面是测试:
![](https://img.haomeiwen.com/i13754622/45c67b1b4659b196.png)
注意:应该会调用两次回调函数,因为will和did进行了两次通知,上面的测试注释掉了will
关闭KVO
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
这样子可以让系统自动关闭对这个属性的KVO,但是没事还是不要手动去实现😒
KVO的注意事项
- KVO只有调用了setter方法才能触发,所以直接修改成员变量的值是不会触发的
这里只是简单地写了下KVO,还有其他深入的知识以后再更
KVC
Key-Value-Coding 键值编码 简单来解释就是----允许开发者通过键名直接来获取属性的值,不管这个值在对象中有没有被暴露出来,是不是私有的都是可以获取到的
参考:
https://www.jianshu.com/p/b9f020a8b4c9
上面的教程讲的足够详细了,这里就挑重点的来讲
- 只要是继承了NSObject的对象,都能使用KVC
- (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
上面四种方式就是KVC经常会使用到的方法
1.设值
寻找顺序:
Set<key> -> _key -> _iskey ->key -> iskey
一般而言都是找到第一步就行了,进行第二步寻找之前要满足的条件是:
+ (BOOL)accessInstanceVariablesDirectly{
return YES;//默认YES
}
直接在对应的类(如果你想通过KVC找某个类中的值,就在这个类)中重写这个方法就行,如果返回的是NO
那么就是不允许直接访问实例变量
如果以上几步出现没有找到的情况,会直接(被KVC的类)调用:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
如果没有重写该方法,默认是抛出异常
如果属性是基本数据类型,比如double,int或者说NSInteger,那么我们在设置的时候要自己做转化,KVC不会自动将数字进行封装,
我们要封装成NSNumber或者NSValue(具体采用哪种视情况而定,基本类型是NSNumber,结构体类型就是NSValue)
[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
2.取值
get<Key>,<key>,is<Key>,(后面开始寻找成员变量名) _<key>,_is<Key>,<key>,is<Key>
教程上的 CountofKey方法看不懂 不管先
如果没有找到key那么抛出异常,调用方法:
- (id)valueForKey:(NSString *)key{
return nil;
}
要注意的是取值的时候,KVC返回的是个id类型的对象,那么对于一些值类型,比如int,在KVC中会先封装成为NSNumber然后在返回一个id对象指向这个NSNumber,所以我们用KVC取得的值总是一个对象.
3.keypath
对于复合型的类,我们可以先取出类中的对象,再用该对象来获取值,但是KVC直接提供keypath的方法来达到同样效果
例子可以查看教程
4.KVC处理异常
- 如果key是int double等基本类型,则不能传入nil,因为这些值本身就不能为空,不像指针(这一点教程上讲的不清楚)
如果传入nil,会抛出异常
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"不能将%@设成nil", key);
}
5.KVC处理集合
KVC除了能直接操作对象的属性,还提供了一些便利的方法来操作集合
对于NSArray(只能存储对象),那么使用KVC好像也说的过去,归根结底KVC还是整个对数组中每个对象做了处理而已,而是个对象就有key-value
KVC对于NSArray提供的处理函数
@avg, @count , @max , @min ,@sum
看一个简单的例子:
NSArray *numberArray = @[@2,@1,@4,@6,@5,@7,@9,@8];
// 最大值
NSInteger max = [[numberArray valueForKeyPath:@"@max.intValue"] integerValue];
// 最小值
NSInteger min = [[numberArray valueForKeyPath:@"@min.intValue"] integerValue];
// 平均值
NSInteger avg = [[numberArray valueForKeyPath:@"@avg.intValue"] integerValue];
// 总和
NSInteger sum = [[numberArray valueForKeyPath:@"@sum.intValue"] integerValue];
注意 使用的是 valueForKeyPath
我知道可能逻辑上有点看不惯,但是可以先这样用就行
可以再难一点就是数组里面的对象的对象,根据最后一个对象的某个属性获取比如说最大值之类的
对象操作运算
@distinctUnionOfObjects
@unionOfObjects
记住第一个就好了,两者都是返回一个NSArray对象,第一个返回的是去重的结果,第二个没点用
参考:https://www.jianshu.com/p/6b32f6279347
6.KVC处理字典
这一点真的是太方便了
我们可以利用KVC提供的方法将字典转成模型,不需要再一个个属性的赋值,也可以直接从模型转成字典,不需要一个个添加到字典去了
![](https://img.haomeiwen.com/i13754622/2a75cf423cec7897.png)
对于模型转成字典,其实就是将每一个数组的元素对对象用一遍KVC而已,只要理解了KVC这两个就不难了
Boss *boss=[[Boss alloc] init];
//模型转成字典
NSArray *arr=@[@"name",@"money"];
NSDictionary *dic=[boss dictionaryWithValuesForKeys:arr];
NSLog(@"%@",dic);
//字典转成模型
NSDictionary *dic2=@{@"name":@"小布",@"money":@"0"};
[boss setValuesForKeysWithDictionary:dic2];
NSLog(@"%@",[boss valueForKey:@"name"]);
![](https://img.haomeiwen.com/i13754622/66b8a40e63f3501c.png)