1.概述
KVO,即:Key-Value Observing,是 Objective-C 对 观察者模式(Observer Pattern)的实现。它提供一种机制,当指定的对象的属性被修改后,观察者就会接受到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。
2.使用
1. 基本使用
KVO本质上是基于runtime的动态分发机制,通过key来监听value的值。
OC能够实现监听因为都遵守了NSKeyValueCoding协议。OC所有的类都是继承自NSObject,其默认已经遵守了该协议,但Swift不是基于runtime的。Swift中继承自NSObject的属性处于性能等方面的考虑,默认是关闭动态分发的, 所以无法使用KVO,只有在属性前加 @objc dynamic 才会开启运行时,允许监听属性的变化。
在Swift3中只需要加上dynamic就可以了,而Swift4以后则还需要@objc
- 注册
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
observer:观察者,也就是KVO通知的订阅者。订阅着必须实现。
keyPath:描述将要观察的属性,相对于被观察者。
options:KVO的一些属性配置;有四个选项。
NSKeyValueObservingOptionNew:change字典包括改变后的值
NSKeyValueObservingOptionOld:change字典包括改变前的值
NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)
- 监听
在观察者内重写这个方法。在属性变化时,观察者则可以在函数内对属性变化做处理。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
- 移除
在不用的时候,不要忘记解除注册,否则会导致内存泄露。
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath;
- 举例
class ObservedClass: NSObject {
// 开启运行时,允许监听属性的变化
@objc dynamic var name: String = "Original"
// age 并不会触发KVO
var age: Int = 18
}
class ViewController: UIViewController {
var observed = ObservedClass()
override func viewDidLoad() {
super.viewDidLoad()
observed.addObserver(self, forKeyPath: "age", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
observed.addObserver(self, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
// 修改属性值,触发KVO
observed.name = "JiangT"
observed.age = 22
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("属性改变了")
print(keyPath)
print("change字典为:")
print(change)
}
}
---输出结果---
属性改变了
Optional("name")
change字典为:
Optional(
[__C.NSKeyValueChangeKey(_rawValue: new): JiangT,
__C.NSKeyValueChangeKey(_rawValue: kind): 1,
__C.NSKeyValueChangeKey(_rawValue: old): Original])
2. 手动KVO 及禁用KVO
- 首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了。
- 其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
- 如果需要禁用该类KVO的话直接automaticallyNotifiesObserversForKey返回NO,实现属性的 setter 方法,不进行调用willChangeValueForKey: 和 didChangeValueForKey方法。
主要方法:
open func willChangeValue(forKey key: String)
open func didChangeValue(forKey key: String)
class func automaticallyNotifiesObservers(forKey key: String) -> Bool
举例
---被观察类---
class ObservedClass: NSObject {
private var _name: String = "Original"
@objc dynamic var name: String {
get {
return _name
}
set (n) {
self.willChangeValue(forKey: "name")
_name = n
self.didChangeValue(forKey: "name")
}
}
override class func automaticallyNotifiesObservers(forKey key: String) -> Bool {
// 设置对该 key 不自动发送通知
if key == "name" {
return false
}
return super.automaticallyNotifiesObservers(forKey: key)
}
}
class ViewController: UIViewController {
var observed = ObservedClass()
override func viewDidLoad() {
super.viewDidLoad()
observed.addObserver(self, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
// 修改属性值,触发KVO
observed.name = "JiangT"
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("属性改变了")
print(keyPath)
print("change字典为:")
print(change)
}
}
---输出结果---
属性改变了
Optional("name")
change字典为:
Optional([__C.NSKeyValueChangeKey(_rawValue: kind): 1,
__C.NSKeyValueChangeKey(_rawValue: old): Original,
__C.NSKeyValueChangeKey(_rawValue: new): JiangT])
3. 实现原理
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.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
大致意思为:
苹果使用了一种isa交换的技术,当ObjectA的被观察后,ObjectA对象的isa指针被指向了一个新建的子类NSKVONotifying_ObjectA,且这个子类重写了被观察值的setter方法和class方法,dealloc和isKVO方法,然后使ObjectA对象的isa指针指向这个新建的类,然后事实上ObjectA变为了NSKVONotifying ObjectA的实例对象,执行方法要从这个类的方法列表里找。
KVO是基于runtime机制实现的。
当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类(如果原类为ObservedClass,那么生成的派生类名为NSKVONotifying_ObservedClass),在这个派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的setter方法内实现真正的通知机制
- 每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类(isa-swizzling,后续Runtime学习记录中展开),从而在给被监控属性赋值时执行的是派生类的setter方法。派生类中还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类。
测试代码
@interface KVOObject : NSObject
@property (nonatomic, copy ) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation KVOObject
- (NSString *)description {
NSLog(@"object address : %p \n", self);
IMP nameIMP = class_getMethodImplementation(object_getClass(self), @selector(setName:));
IMP ageIMP = class_getMethodImplementation(object_getClass(self), @selector(setAge:));
NSLog(@"object setName: IMP %p object setAge: IMP %p \n", nameIMP, ageIMP);
Class objectMethodClass = [self class];
Class objectRuntimeClass = object_getClass(self);
Class superClass = class_getSuperclass(objectRuntimeClass);
NSLog(@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \n", objectMethodClass, objectRuntimeClass, superClass);
NSLog(@"object method list \n");
unsigned int count;
Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
for (NSInteger i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"method Name = %@\n", methodName);
}
return @"";
}
在另一个类中分别创建两个KVOObject对象,其中一个对象被观察者通过KVO的方式监听,另一个对象则始终没有被监听。在KVO前后分别打印两个对象的关键信息,看KVO前后有什么变化。
@property (nonatomic, strong) KVOObject *object1;
@property (nonatomic, strong) KVOObject *object2;
self.object1 = [[KVOObject alloc] init];
self.object2 = [[KVOObject alloc] init];
[self.object1 description];
[self.object2 description];
[self.object1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object1 description];
[self.object2 description];
self.object1.name = @"lxz";
self.object1.age = 20;
下面是KVO前后的打印信息
// 第一次
object address : 0x604000239340
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
// 第二次
object address : 0x604000239340
object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA
object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
我们发现对象被KVO后,其真正类型变为了NSKVONotifying_KVOObject类,已经不是之前的类了。KVO会在运行时动态创建一个新类,将对象的isa指向新创建的类,新类是原类的子类,命名规则是NSKVONotifying_xxx的格式。KVO为了使其更像之前的类,还会将对象的class实例方法重写,使其更像原类。
自定义kvo实现
static void *const zs_KVOObserverAssociatedKey = (void *)&zs_KVOObserverAssociatedKey;
static NSString *zs_KVOClassPrefix = @"zs_KVONotifying_";
@implementation KVOObserverItem
- (instancetype)initWithObserver:(NSObject *)observer
key:(NSString *)key
block:(zs_KVOObserverBlock)block {
self = [super init];
if (self) {
self.observer = observer;
self.key = key;
self.block = block;
}
return self;
}
@end
- (void)zs_addObserver:(NSObject *)observer
keyPath:(NSString *)keyPath
callback:(zs_KVOObserverBlock)callback {
// 1. 通过Method判断是否有这个key对应的selector,如果没有则Crash。
SEL originalSetter = NSSelectorFromString(zs_setterForGetter(keyPath));
Method originalMethod = class_getInstanceMethod(object_getClass(self), originalSetter);
if (!originalMethod) {
NSString *exceptionReason = [NSString stringWithFormat:@"%@ Class %@ setter SEL not found.", NSStringFromClass([self class]), keyPath];
NSException *exception = [NSException exceptionWithName:@"NotExistKeyExceptionName" reason:exceptionReason userInfo:nil];
[exception raise];
}
// 2. 判断当前类是否是KVO子类,如果不是则创建,并设置其isa指针。
Class kvoClass = object_getClass(self);
NSString *kvoClassString = NSStringFromClass(kvoClass);
if (![kvoClassString hasPrefix:zs_KVOClassPrefix]) {
kvoClass = [self zs_makeKVOClassWithName:kvoClassString];
object_setClass(self, kvoClass);
}
// 3. 如果没有实现,则添加Key对应的setter方法。
if (![self zs_hasMethodWithKey:originalSetter]) {
class_addMethod(kvoClass, originalSetter, (IMP)zs_kvoSetter, method_getTypeEncoding(originalMethod));
}
// 4. 将调用对象添加到数组中。
KVOObserverItem *observerItem = [[KVOObserverItem alloc] initWithObserver:observer key:keyPath block:callback];
NSMutableArray<KVOObserverItem *> *observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
if (observers == nil) {
observers = [NSMutableArray array];
}
[observers addObject:observerItem];
objc_setAssociatedObject(self, zs_KVOObserverAssociatedKey, observers, OBJC_ASSOCIATION_RETAIN);
}
- (void)zs_removeObserver:(NSObject *)observer
keyPath:(NSString *)keyPath {
NSMutableArray <KVOObserverItem *>* observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
[observers enumerateObjectsUsingBlock:^(KVOObserverItem * _Nonnull mapTable, NSUInteger idx, BOOL * _Nonnull stop) {
if (mapTable.observer == observer && keyPath == mapTable.key) {
[observers removeObject:mapTable];
}
}];
}
#pragma mark - ----- Private Method Or Funcation ------
static void zs_kvoSetter(id self, SEL selector, id value) {
// 1. 获取旧值。
id (*getterMsgSend) (id, SEL) = (void *)objc_msgSend;
NSString *getterString = zs_getterForSetter(selector);
SEL getterSelector = NSSelectorFromString(getterString);
id oldValue = getterMsgSend(self, getterSelector);
// 2. 创建super的结构体,并向super发送属性的消息。
id (*msgSendSuper) (void *, SEL, id) = (void *)objc_msgSendSuper;
struct objc_super objcSuper = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
msgSendSuper(&objcSuper, selector, value);
// 3. 遍历调用block。
NSMutableArray <KVOObserverItem *>* observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
[observers enumerateObjectsUsingBlock:^(KVOObserverItem * _Nonnull mapTable, NSUInteger idx, BOOL * _Nonnull stop) {
if ([mapTable.key isEqualToString:getterString] && mapTable.block) {
mapTable.block(self, NSStringFromSelector(selector), oldValue, value);
}
}];
}
- (BOOL)zs_hasMethodWithKey:(SEL)key {
NSString *setterName = NSStringFromSelector(key);
unsigned int count;
Method *methodList = class_copyMethodList(object_getClass(self), &count);
for (NSInteger i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
if ([methodName isEqualToString:setterName]) {
return YES;
}
}
return NO;
}
static NSString * zs_getterForSetter(SEL setter) {
NSString *setterString = NSStringFromSelector(setter);
if (![setterString hasPrefix:@"set"]) {
return nil;
}
NSString *getterString = [setterString substringWithRange:NSMakeRange(4, setterString.length - 5)];
NSString *firstString = [setterString substringWithRange:NSMakeRange(3, 1)];
firstString = [firstString lowercaseString];
getterString = [NSString stringWithFormat:@"%@%@", firstString, getterString];
return getterString;
}
static NSString * zs_setterForGetter(NSString *getter) {
NSString *getterString = getter;
NSString *firstString = [getterString substringToIndex:1];
firstString = [firstString uppercaseString];
NSString *setterString = [getterString substringFromIndex:1];
setterString = [NSString stringWithFormat:@"set%@%@:", firstString, setterString];
return setterString;
}
- (Class)zs_makeKVOClassWithName:(NSString *)name {
// 1. 判断是否存在KVO类,如果存在则返回。
NSString *className = [NSString stringWithFormat:@"%@%@", zs_KVOClassPrefix, name];
Class kvoClass = objc_getClass(className.UTF8String);
if (kvoClass) {
return kvoClass;
}
// 2. 如果不存在,则创建KVO类。
kvoClass = objc_allocateClassPair(object_getClass(self), className.UTF8String, 0);
objc_registerClassPair(kvoClass);
// 3. 重写KVO类的class方法,指向自定义的IMP。
Method method = class_getInstanceMethod(object_getClass(self), @selector(class));
const char *types = method_getTypeEncoding(method);
class_addMethod(kvoClass, @selector(class), (IMP)zs_kvoClass, types);
return kvoClass;
}
static Class zs_kvoClass(id self, SEL selector) {
return class_getSuperclass(object_getClass(self));
}