[关闭]
@qidiandasheng 2018-09-17T16:21:52.000000Z 字数 5423 阅读 3128

KVC 和 KVO

iOS连载


前言

其实KVC和KVO的关系不大,简单的说KVC是一种键值编码,而KVO只是通过键值编码来对成员变量进行观察,可以说KVO只是使用了KVC。

KVC

简介

KVC:全称是Key-value coding,翻译成键值编码。它提供了一种使用字符串而不是访问器方法去访问一个对象实例变量的机制。Foundation框架中,NSObject有个叫NSKeyValueCoding的分类,里面就包含了以下这些KVC方法:

最常用的是以下几个:

  1. - (id)valueForKey:(NSString *)key;
  2. - (void)setValue:(id)value forKey:(NSString *)key;
  3. - (id)valueForKeyPath:(NSString *)keyPath;
  4. - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

详细使用

关于KVC更细节的使用可以查看官方文档

中文有篇写的比较详细的可以参考KVC原理剖析

优点

所以KVC其实就是根据key字符串来处理成员变量的一种方式,但是我们原来就可以使用点语法来set和get成员变量啊,那为什么还要用KVC呢?

肯定是因为KVC有一些优点是点语法直接访问做不到的。

  1. 访问私有成员变量:我们知道当属性定义在.h文件里的时候,我们是能够通过属性来访问的。但是如果是定义在.m文件里,那么我们就无法直接访问到了,这时如果知道私有成员变量名,就可以通过KVC就可以进行访问了
  2. 简化字典转模型代码:可以使用setValuesForKeysWithDictionary简单的一步就把字典里的每一项赋值给你实体类对应的属性。

KVC调用的是什么

KVC是直接对成员变量赋值,还是调用这个成员变量对应的setter和getter方法?

定义一个Person类:

  1. #import "Person.h"
  2. @implementation Person{
  3. NSString *_name;
  4. }
  5. - (void)setName:(NSString *)name{
  6. _name = name;
  7. NSLog(@"调用name的setter方法");
  8. }
  9. - (NSString *)name{
  10. NSLog(@"调用name的getter");
  11. return _name;
  12. }
  13. @end

执行KVC代码:

  1. Person *my = [[Person alloc] init];
  2. [my setValue:@"齐滇大圣" forKey:@"name"];
  3. [my valueForKey:@"name"];

输出:

  1. 2018-09-17 14:33:51.712400+0800 test[89726:6843737] 调用namesetter方法
  2. 2018-09-17 14:33:51.712581+0800 test[89726:6843737] 调用namegetter

我们能看到会调用对应的settergetter方法。


如果我们把settergetter去掉,再看看会不会对成员变量进行赋值处理。

类定义:

  1. #import "Person.h"
  2. @implementation Person{
  3. NSString *_name;
  4. }
  5. @end

我们能看到调用时,_name是有值的:

结论:

所以这里就是上面所说的一个KVC的搜索规则问题,比如它会去先搜索settergetter方法,看看有没有。比如getter有几种写法都是符合的,但有一个搜索的先后顺序get<Key>, <key>, is<Key>, _<key>。具体的搜索规则可以查看上面这篇文章的介绍。

KVO

简介

KVO:全称是Key-value observing,翻译成键值观察。KVO中有一种就是基于KVC实现的。

Foundation里关于KVO的部分都定义在NSKeyValueObserving.h中,KVO通过以下三个NSObject分类实现。

  1. NSObject(NSKeyValueObserving)
  2. NSObject(NSKeyValueObserverRegistration)
  3. NSObject(NSKeyValueObservingCustomization)

下面这是添加观察者和解除观察者的方法,定义在NSObject(NSKeyValueObserverRegistration)中,也是我们在工程中常用的两个方法。注意添加了观察者一定要有解除观察者的对应实现,否则的话会导致资源泄露崩溃。

  1. - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
  2. - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

手动实现KVO

手动设置键值观察主要是以下两个方法:

  1. - (void)willChangeValueForKey:(NSString *)key
  2. - (void)didChangeValueForKey:(NSString *)key

例子:

  1. @interface Target : NSObject
  2. {
  3. int age;
  4. }
  5. // for manual KVO - age
  6. - (int) age;
  7. - (void) setAge:(int)theAge;
  8. @end
  9. @implementation Target
  10. - (id) init
  11. {
  12. self = [super init];
  13. if (nil != self)
  14. {
  15. age = 10;
  16. }
  17. return self;
  18. }
  19. // for manual KVO - age
  20. - (int) age
  21. {
  22. return age;
  23. }
  24. - (void) setAge:(int)theAge
  25. {
  26. [self willChangeValueForKey:@"age"];
  27. age = theAge;
  28. [self didChangeValueForKey:@"age"];
  29. }
  30. + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
  31. if ([key isEqualToString:@"age"]) {
  32. return NO;
  33. }
  34. return [super automaticallyNotifiesObserversForKey:key];
  35. }
  36. @end

首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey:didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;

其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。

KVO实现原理

键值观察其实是通过Objective-C 强大的 Runtime 动态能力实现的。

KVO的源码并不是开源的,但有一套GNU的实现,可以给我们提供一下思路。GNU的下载地址。具体的关于源码的解释可以参考带着问题读源码----KVO。下面是一个简单的实现过程的描述:

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。

派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。

同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

我们来打个比方,比如之前有个类叫做Person,那么当它被监听之后Runtime会动态的一个派生类叫做NSKVONotifying_Person

新的派生类会重写以下方法:
增加了监听的属性对应的set方法,class,dealloc,_isKVOA。

1:class

重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。

  1. NSLog(@"self->isa:%@",self->isa);
  2. NSLog(@"self class:%@",[self class]);

在建立KVO监听前,打印结果为:

  1. self->isa:Person
  2. self class:Person

在建立KVO监听之后,打印结果为:

  1. self->isa:NSKVONotifying_Person
  2. self class:Person

可以看出建立监听之后isa指针指向的类变了。

2:重写set方法

新类会重写对应的set方法,是为了在set方法中增加另外两个方法的调用:

  1. - (void)willChangeValueForKey:(NSString *)key
  2. - (void)didChangeValueForKey:(NSString *)key

其中,didChangeValueForKey:方法负责调用:

  1. - (void)observeValueForKeyPath:(NSString *)keyPath
  2. ofObject:(id)object
  3. change:(NSDictionary *)change
  4. context:(void *)context

3:_isKVOA

这个私有方法估计是用来标示该类是一个 KVO 机制声称的类。


总之要实现KVO由三种方式:

如何优雅地使用 KVO

在使用KVO时有这么几个问题:

  1. 需要手动移除观察者,且移除观察者的时机必须合适;
  2. 注册观察者的代码和事件发生处的代码上下文不同,传递上下文是通过 void * 指针;
  3. 需要覆写 -observeValueForKeyPath:ofObject:change:context: 方法,比较麻烦;
  4. 在复杂的业务逻辑中,准确判断被观察者相对比较麻烦,有多个被观测的对象和属性时,需要在方法中写大量的 if 进行判断;

这里介绍一个facebook的开源库KVOController,简单的使用如下:

  1. // create KVO controller with observer
  2. FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
  3. self.KVOController = KVOController;
  4. // observe clock date property
  5. [self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
  6. // update clock view with new value
  7. clockView.date = change[NSKeyValueChangeNewKey];
  8. }];

优点:

  1. 不用手动移除观察者,在KVOController释放时,所有它自己持有的所有 KVO 的观察者交由会交由_FBKVOSharedControllerunobserve:info:进行释放
  2. 所有当前的观察对象的值的改变,都会在当前的block里面进行回调,一个被观察者一个block,不用复写-observeValueForKeyPath:ofObject:change:context:并进行大量的if判断了。

KVOController的代码解析可以阅读:如何优雅地使用 KVO

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注