@qidiandasheng
2018-09-17T16:21:52.000000Z
字数 5423
阅读 3128
iOS连载
其实KVC和KVO的关系不大,简单的说KVC是一种键值编码,而KVO只是通过键值编码来对成员变量进行观察,可以说KVO只是使用了KVC。
KVC:全称是Key-value coding
,翻译成键值编码。它提供了一种使用字符串而不是访问器方法去访问一个对象实例变量的机制。Foundation框架中,NSObject有个叫NSKeyValueCoding
的分类,里面就包含了以下这些KVC方法:
最常用的是以下几个:
- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
关于KVC更细节的使用可以查看官方文档。
中文有篇写的比较详细的可以参考KVC原理剖析:
所以KVC其实就是根据key字符串来处理成员变量的一种方式,但是我们原来就可以使用点语法来set和get成员变量啊,那为什么还要用KVC呢?
肯定是因为KVC有一些优点是点语法直接访问做不到的。
setValuesForKeysWithDictionary
简单的一步就把字典里的每一项赋值给你实体类对应的属性。KVC是直接对成员变量赋值,还是调用这个成员变量对应的setter和getter方法?
定义一个Person类:
#import "Person.h"
@implementation Person{
NSString *_name;
}
- (void)setName:(NSString *)name{
_name = name;
NSLog(@"调用name的setter方法");
}
- (NSString *)name{
NSLog(@"调用name的getter");
return _name;
}
@end
执行KVC代码:
Person *my = [[Person alloc] init];
[my setValue:@"齐滇大圣" forKey:@"name"];
[my valueForKey:@"name"];
输出:
2018-09-17 14:33:51.712400+0800 test[89726:6843737] 调用name的setter方法
2018-09-17 14:33:51.712581+0800 test[89726:6843737] 调用name的getter
我们能看到会调用对应的setter
和getter
方法。
如果我们把setter
和getter
去掉,再看看会不会对成员变量进行赋值处理。
类定义:
#import "Person.h"
@implementation Person{
NSString *_name;
}
@end
我们能看到调用时,_name
是有值的:
结论:
所以这里就是上面所说的一个KVC的搜索规则问题,比如它会去先搜索setter
和getter
方法,看看有没有。比如getter有几种写法都是符合的,但有一个搜索的先后顺序get<Key>
, <key>
, is<Key>
, _<key>
。具体的搜索规则可以查看上面这篇文章的介绍。
KVO:全称是Key-value observing
,翻译成键值观察。KVO中有一种就是基于KVC实现的。
Foundation
里关于KVO的部分都定义在NSKeyValueObserving.h
中,KVO通过以下三个NSObject分类实现。
NSObject(NSKeyValueObserving)
NSObject(NSKeyValueObserverRegistration)
NSObject(NSKeyValueObservingCustomization)
下面这是添加观察者和解除观察者的方法,定义在NSObject(NSKeyValueObserverRegistration)
中,也是我们在工程中常用的两个方法。注意添加了观察者一定要有解除观察者的对应实现,否则的话会导致资源泄露崩溃。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
手动设置键值观察主要是以下两个方法:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
例子:
@interface Target : NSObject
{
int age;
}
// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;
@end
@implementation Target
- (id) init
{
self = [super init];
if (nil != self)
{
age = 10;
}
return self;
}
// for manual KVO - age
- (int) age
{
return age;
}
- (void) setAge:(int)theAge
{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey:
和 didChangeValueForKey
方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;
其次,要实现类方法 automaticallyNotifiesObserversForKey
,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
键值观察其实是通过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方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。
NSLog(@"self->isa:%@",self->isa);
NSLog(@"self class:%@",[self class]);
在建立KVO监听前,打印结果为:
self->isa:Person
self class:Person
在建立KVO监听之后,打印结果为:
self->isa:NSKVONotifying_Person
self class:Person
可以看出建立监听之后isa
指针指向的类变了。
2:重写set方法
新类会重写对应的set方法,是为了在set方法中增加另外两个方法的调用:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
其中,didChangeValueForKey:方法负责调用:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
3:_isKVOA
这个私有方法估计是用来标示该类是一个 KVO 机制声称的类。
总之要实现KVO由三种方式:
使用KVC
没用访问器方法,运行时会在setValue:forKey方法中调用will/didChangeValueForKey:方法。
有访问器方法
运行时会重写访问器方法调用will/didChangeValueForKey:方法。
因此,直接调用访问器方法改变属性值时,KVO也能监听到。
显示调用will/didChangeValueForKey:方法。
在使用KVO时有这么几个问题:
-observeValueForKeyPath:ofObject:change:context:
方法,比较麻烦;这里介绍一个facebook的开源库KVOController,简单的使用如下:
// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;
// observe clock date property
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
// update clock view with new value
clockView.date = change[NSKeyValueChangeNewKey];
}];
优点:
_FBKVOSharedController
的unobserve:info:
进行释放block
里面进行回调,一个被观察者一个block
,不用复写-observeValueForKeyPath:ofObject:change:context:
并进行大量的if判断了。KVOController的代码解析可以阅读:如何优雅地使用 KVO。