概述
KVO
(Key-Value Observing)
是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于 KVO 的实现机制,所以对属性才会发生作用,一般继承
自 NSObject 的对象都默认支持 KVO。KVO 和 NSNotificationCenter 都是 iOS 中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO 是
一对一
的,而NSNotificationCenter是一对多
的。KVO 对被监听对象无侵入性,不需要修改其内部代码即可实现监听。KVO 的实现依赖于 OC 强大的
Runtime
KVO 是 Cocoa 提供的一种基于 KVC 的机制
实现原理
KVO 是通过
isa-swizzling
技术实现的(这句话是整个 KVO 实现的重点)。当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个
派生类(子类)
,在这个派生类中重写基类(父类)
中任何被观察属性的setter 方法
。派生类在被重写的setter方法内实现真正的通知机制。-
如果原类为Person,那么生成的派生类名为
NSKVONotifying_Person
,每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察监听,那么系统会偷偷将isa指针指向动态生成的派生类
,从而在给被监控属性赋值时
执行的是派生类的setter方法
。
键值观察通知依赖于NSObject 的两个方法:
willChangeValueForKey:
和didChangevlueForKey:
在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就会记录旧的值。然后调用父类的setter
方法更新key的值,而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。KVO的这套实现机制中苹果还偷偷
重写了-class()方法
,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类
使用
- 注册观察者对象 :给对象Person的实例
person(被观察者)
通过-addObserver:forKeyPath:options:context:
添加观察者observer
,指定要观察的属性keyPath
,观察者就可以接受keyPath属性的变化事件。change含有kind、old、new三种
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew , //接受新值 默认
NSKeyValueObservingOptionOld , //接受旧值
NSKeyValueObservingOptionInitial , //在注册时立即接受一次回调,在改变时也会发送通知
NSKeyValueObservingOptionPrior //改变前发送一次,改变后发送一次
};
在调用addObserver后,KVO并不会对观察者进行强引用
- 在观察者中实现
-observeValueForKeyPath:ofObject:change:context:
方法,当keyPath变化时,KVO就会调用该方法通知观察者。object为被观察者对象。
/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
kind指值得变化方式
*/
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, //通过观察设置方法
NSKeyValueChangeInsertion = 2, //通过观察插入方法(容器)
NSKeyValueChangeRemoval = 3, //remove
NSKeyValueChangeReplacement = 4,//替换
};
context 传入任意类型的对象,在接受消息会调的代码中姐可以接受到该对象
- 当观察者不需要监听时,使用
-removeObserver: forKeyPath:
移除KVO。要在观察者消失前调用,-dealloc
中。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
/**
根据 keyPath 确定KVO的出发模式是手动还是自动
手动模式在key变化前后s需要手动调用 -willChangeValueForKey 和 -willChangeValueForKey
[_person willChangeValueForKey:@"name"];
_person.name = [NSString stringWithFormat:@"%d", a++];
[_person willChangeValueForKey:@"name"];
*/
if ([key isEqualToString:NSStringFromSelector(@selector(name))]) {
return NO;
}
return YES;
}
/**
属性依赖键 使对象获取其他对象的特定属性变化的通知机制
*/
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keypaths = [super keyPathsForValuesAffectingValueForKey:key];
/**
当对当前类的dog属性添加观察者时,告诉系统dog的变化依赖于dog的那些属性的变化
*/
if ([key isEqualToString:@"dog"]) {
keypaths = [NSSet setWithObjects:@"_dog.color", @"_dog.age", nil];
}
return keypaths;
}
自定义
添加观察者
- 通过Method判断是否有这个key对应的setter selector(因为值的变化是重写setter方法来达到目的,所以setter必须有),如果没有则Crash。
- 判断当前类是否是KVO子类,如果不是 则创建,并设置其isa指针。(实例的isa指针指向该实例的类,类的isa指针指向它的元类)
- 如果没有实现,则添加Key对应的setter方法。
- 将调用对象添加到数组中。
// 1.
SEL setSelector = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(object_getClass(self), setSelector);
if (!setterMethod) {
NSString *reason = [NSString stringWithFormat:@"Object %@ dose not have a setter for key : %@",self, keyPath];
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
return;
}
// 2.
Class class = object_getClass(self);
NSString *classname = NSStringFromClass(class);
if (![classname hasPrefix:KXSKVOClassPrefix]) {
class = [self makeKvoClassWithOriginalClassName:classname];
object_setClass(self, class);
}
// 3.
if (![self hasSelector:setSelector]) {
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(class, setSelector, (IMP)kvo_setter, types);
}
// 4.
KXSObservationInfo *observerInfo = [[KXSObservationInfo alloc] initWithObserver:observer key:keyPath changeBlock:changeBlock];
NSMutableArray *observerArr = objc_getAssociatedObject(self, KXSKVOObserverProperty);
if (!observerArr) {
observerArr = [NSMutableArray array];
}
[observerArr addObject:observerInfo];
objc_setAssociatedObject(self, KXSKVOObserverProperty, observerArr, OBJC_ASSOCIATION_COPY);
创建子类,并设置其isa指针
- 判断是否存在KVO类,如果存在则返回。
- 如果不存在,则创建KVO类。
- 重写KVO类的class方法,指向自定义的IMP。
// 1.
NSString *kvoClazzName = [kISKVOClassPrefix stringByAppendingString:originalClazzName];
Class clazz = NSClassFromString(kvoClazzName);
if (clazz) {
return clazz;
}
// 2.
// class doesn't exist yet, make it
Class originalClazz = object_getClass(self);
/**
创建一个类 为"class pair"分配空间
@param superclass 继承的类
@param name 类名
objc_allocateClassPair(Class superclass, const char * name, 0)
*/
Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
// 3.
// grab class method's signature so we can borrow it 获取类中的某个实例方法(减号方法)
Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
const char *types = method_getTypeEncoding(clazzMethod);
// types 返回值类型 -> @"v@;@"
// [p add] [p setName:name]
// objc_msgSend(p, @selector(add)) objc_msgSend(p, @selector(setName), name)
class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
// SEL selector 的简写,俗称方法选择器,实质存储的是方法的名称
// IMP implement 的简写,俗称方法实现,看源码得知它就是一个函数指针
// Method 对上述两者的一个包装结构.
objc_registerClassPair(kvoClazz);
重写子类的setter方法
- 获取旧值。
- 创建super的结构体,并向super发送属性的消息。这样相当于是调用原来类的setter方法,这一步是必须的。
- 遍历调用回调。
// 1.
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = getterForSetter(setterName);
if (!getterName) {
NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
return;
}
id oldValue = [self valueForKey:getterName];
// 2.
struct objc_super superclazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
// cast our pointer so the compiler won't complain
void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
// call super's setter, which is original class's setter method
// objc_msgSend(objc_super->receiver, _cmd)
// 从父类的方法列表开始找 _cmd 方法
objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
// 3.
// look up observers and call the blocks
NSMutableArray *observers = objc_getAssociatedObject(self,kISKVOAssociatedObservers);
for (ISObservationInfo *each in observers) {
if ([each.key isEqualToString:getterName]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
each.block(self, getterName, oldValue, newValue);
});
}
}
重写class方法调用
static Class kvo_class(id self, SEL _cmd)
{
// 调用object_getClass函数后其返回的是一个Class类型,Class是objc_class定义的一个typedef别名,通过objc_class就可以获取到对象的isa指针指向的Class,也就是对象的类对象
// 返回 类的父类 class_getSuperclass
// 起到隐藏该子类,以“欺骗”外部调用者它就是起初的那个类
return class_getSuperclass(object_getClass(self));
}
参考
facebook 的 KVOController
KVO底层原理及Block方式回调实现
KVC/KVO原理详解及编程指南
iOS开发-Runtime详解()
iOS开发-Runloop详解()
Type Encodings
super与objc_msgSendSuper
KVC/KVO原理详解及编程指南