KVO是IOS中一种强大且有效的机制,当一个对象的属性发生变化时,注册成为这个对象的观察者的其他对象可以收到通知。我们可以使用KVO来观察对象属性的变化。比如,想实现下拉刷新效果时,可以使用KVO观察UITableView的contenOffset属性的变化来实现的。
In order to be considered KVO-compliant for a specific property, a class must ensure the following:
- The class must be key-value coding compliant for the property, as specified in Ensuring KVC Compliance.KVO supports the same data types as KVC, including Objective-C objects and the scalars and structures listed in Scalar and Structure Support
- The class emits KVO change notifications for the property.
- Dependent keys are registered appropriately (see Registering Dependent Keys).
文档里提到了,要使一个类的属性支持KVO,这个类对于属性是满足KVC的,并且这个类会发送KVO的通知。
有两种技术来确保发送KVO通知:
Automatic support :自动支持是NSObject提供的,对于支持KVC的属性默认都是可用的。不需要写额外的代码来发送属性改变的通知。
Manual change notification:手动发送通知需要额外的代码。
Automatic Change Notification
NSObject provides a basic implementation of automatic key-value change notification. Automatic key-value change notification informs observers of changes made using key-value compliant accessors, as well as the key-value coding methods.
NSObject实现了自动改变的通知。自动通知有两种,一种是使用属性的setter方法,一种是使用KVC。
KVO的原理
KVO的实现依赖于runtime,Apple文档里提到过KVO的实现
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
苹果使用了 isa-swizzling来实现KVO。当给一个被观察者的属性添加观察者后,被观察者的isa指针会被改变,指向一个中间的class,而不是原来真正的class。具体来说,会创建一个新的类,这个类继承自被观察者,并且重写了被观察的属性的setter方法,重写的setter方法会在负责调用原setter方法的前后,通知所有观察者值得变化(使用willChangeValueForKey
和didChangeValueForKey
来通知),并且把isa的指针指向这个新创建的子类。
-(void)setName:(NSString *)newName{
[self willChangeValueForKey:@"name"]; //KVO在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO在调用存取方法之后总调用}
看一下下面的测试代码:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
@end
#import <objc/runtime.h>
#import "Person.h"
int main(int argc, char * argv[]) {
Person *p = [[Person alloc] init];
PrintDescriptionid(p);
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
PrintDescriptionid(p);
return 0;
}
static NSArray *ClassMethodNames(Class c)
{
NSMutableArray *array = [NSMutableArray array];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++)
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
free(methodList);
return array;
}
static void PrintDescriptionid( id obj)
{
NSString *str = [NSString stringWithFormat:
@"NSObject class %s\nLibobjc class %s\nSuper Class %s\nimplements methods <%@>",
class_getName([obj class]),
class_getName(object_getClass(obj)),
class_getName(class_getSuperclass(object_getClass(obj))),
[ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
log:
//添加观察者之前
NSObject class Person
Libobjc class Person
Super Class NSObject
implements methods <.cxx_destruct, name, setName:>
//添加观察者之后
NSObject class Person
Libobjc class NSKVONotifying_Person
Super Class Person
implements methods <setName:, class, dealloc, _isKVOA>
object_getClass(obj)会获取obj对象isa指向的类。从log可以看出,添加观察者后,obj对象isa指针指向NSKVONotifying_Person
这个类,它的父类是Person
,并且NSKVONotifying_Person
里实现了<setName:, class, dealloc, _isKVOA>
这个几个方法。
Manual Change Notification
如果你想完全控制一个属性的通知,需要重写automaticallyNotifiesObserversForKey:
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
BOOL automatic = NO;
if ([key isEqualToString:@"name"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
在运行一下之前的代码log:
//添加KVO之前
Libobjc class Person
Super Class NSObject
implements methods <.cxx_destruct, name, setName:>
//添加KVO之后
NSObject class Person
Libobjc class Person
Super Class NSObject
implements methods <.cxx_destruct, name, setName:>
此时也不会创建NSKVONotifying_Person
这个类了。为了实现手动发送通知,这是在改变值之前要调用 willChangeValueForKey:,改变值之后调用didChangeValueForKey:
KVO的使用:
[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
这个方法是给tableView添加了一个观察者来监测tableView的contentOffset属性的变化。这个方法不会增加方法的调用者(self.tableView)和观察者(self)的引用计数。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@", change);
}
在观察者里实现这个方法,当被观察者的被观察的属性发生变化时,会调用这个方法。
最后,不要忘记移除观察者:
- (void)dealloc
{
[self.tableView removeObserver:self forKeyPath:@"contentOffset"];
}
FBKVOController
facebook开源的FBKVOController框架可以很方便地使用KVO。
使用FBKVOController,上面的代码可以改写成:
[self.KVOController observe:self.tableView keyPath:@"contentOffset" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
NSLog(@"%@", change);
}];
在FBKVOController,有一个NSObject的category,里面给NSObject添加了两个属性
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
使用KVOController时会对被观察的对象强引用,使用KVOControllerNonRetaining对被观察的对象是弱引用。
FBKVOController类里有一个实例变量:
NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
NSMapTable 的key存储的是被观察的对象,在初始化方法里可以设置成强引用或者弱引用的。它的value存放的是_FBKVOInfo对象,主要是关于被观察者的keyPath等信息。
FBKVOController使用-observer:keyPath:options:block:
观察对象属性变化时,用到了_FBKVOSharedController这个类,这个类是一个单例,它的实例方法添加了观察者:
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register info
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
// add observer
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
if (info->_state == _FBKVOInfoStateInitial) {
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
它也实现了观察变化的方法:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
_FBKVOInfo *info;
{
// lookup context in registered infos, taking out a strong reference only if it exists
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}
if (nil != info) {
// take strong reference to controller
FBKVOController *controller = info->_controller;
if (nil != controller) {
// take strong reference to observer
id observer = controller.observer;
if (nil != observer) {
// dispatch custom block or action, fall back to default action
if (info->_block) {
NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
当KVOController调用dealloc时,会移除观察者。