KVO (Key-Value Observing)
KVO是 Objective-C 对观察者模式(Observer Pattern)的实现。也是 Cocoa Binding 的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知。
KVO 实现机制
KVO 的实现也依赖于 Objective-C 强大的 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.
You should never rely on the
isa
pointer to determine class membership. Instead, you should use theclass
method to determine the class of an object instance.
Apple看起来并不希望过多暴露KVO实现细节,文档都是一笔带过,唯一有用的信息也就是:被观察对象的 isa 指针会指向一个中间类,而不是原来真正的类。不过,如果用 runtime 提供的方法去深入挖掘,所有被掩盖的细节都会原形毕露。
KVO的实现原理:
当观察一个对象时,一个当前类的子类会被动态创建(如果当前类为Dog,动态创建的子类就是NSKVONotifying_Dog)。这个新的子类重写了被观察属性keyPath的setter方法,被观察的属性发生改变之前,willChangeValueForKey:
被调用,通知系统该 keyPath 的属性值即将改变;当改变发生后, didChangeValueForKey:
被调用,通知系统该 keyPath 的属性值已经改变;然后observeValueForKey:ofObject:change:context:
会被调用。最后把这个对象的 isa 指针 ( isa指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。Apple重写了 class方法,隐藏新创建的子类,通过 class 方法获取的还是原来的类。
KVO缺陷
-
只能重写
-observeValueForKeyPath:ofObject:change:context:
方法来获得通知,不能使用自定义selector -
如果父类同样监听同一个对象的同一个属性,但并不想让父类也做出响应,这时需要使用 context来进行区分,在
-addObserver:forKeyPath:options:context:
传进去一个父类不知道的context 就成实现,这样虽然能实现,但是很繁琐。 -
如果观察几个不同的属性,就只能在
-observeValueForKeyPath:ofObject:change:context:
对 keyPath做判断,好多代码写在一起,代码逻辑变得有些复杂。
自定义实现KVO
1.创建NSObjec的category,添加自定义方法
#import <Foundation/Foundation.h>
/**
* @param observedObject asd 需要被观察的对象
* @param observedKey 观察的属性
* @param oldValue 属性旧值
* @param currentValue 属性新值
*/
typedef void(^XMObservingBlock)(id observedObject, NSString * observedKey, id oldValue, id currentValue);
@interface NSObject (KVO)
/**
* 添加观察者
* @param observer 需要添加的观察者
* @param keyPath 观察的属性
* @param block 属性变化后执行的回调
*/
- (void)XM_addObserver:(NSObject *)observer forKey:(NSString *)keyPath withBlock:(XMObservingBlock)block;
/**
* 移除观察者
* @param observer 需要移除的观察者
* @param key 观察的属性
*/
- (void)XM_removeObserver:(NSObject *)observer forKey:(NSString *)key;
@end
2.导入头文件import <objc/message.h>
,并声明如下变量
static NSString * const kXMKVOClassPrefix = @"XMKVONotifying_";
static NSString * const kXMKVOObservations = @"kXMKVOObservations";
3.实现XM_addObserver:forKey:withBlock:
实现思路如下:
1).根据 key得到set方法,判断对象的类有没有相应的set方法,如果没有则返回。
2).获取当前类的name ,如果当前类不是 kvo子类 ,那么就去生成 kvo子类,然后让 isa 指向 kvo子类
3).如果 kvo子类没有对应的set方法,则添加自定义的set方法。(同一个key可能会被添加多次)
4).添加观察者集合,并关联观察者集合的数组,存储所有的观察者集合
//添加观察者
- (void)XM_addObserver:(NSObject *)observer forKey:(NSString *)keyPath withBlock:(XMObservingBlock)block{
// 检查对象的类有没有相应的 setter 方法
SEL setterSelector = NSSelectorFromString([self setter:keyPath]);
// 因为重写了 class,所以[self class]获取的一直是父类
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
if (!setterMethod) {
NSLog(@"keyPath没有相应的set方法");
return;
}
// 获取当前类的 name
Class clazz = object_getClass(self);
NSString * clazzName = NSStringFromClass(clazz);
// 如果当前类不是 kvo子类。(如果添加了多次观察者,kvo子类在第一次添加观察者的时候就创建了)
if (![clazzName hasPrefix:kXMKVOClassPrefix]) {
// 生成 kvo子类
clazz = [self createKVOClassWithOriginalClassName:clazzName];
// 让 isa 指向 kvo子类
object_setClass(self, clazz);
}
// 如果 kvo子类 没有对应的 setter 方法,则添加。(同一个 key 可能会被添加多次)
if (![self hasSelector:setterSelector]) {
const char * types = method_getTypeEncoding(setterMethod);
class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
}
// 创建观察者组合
XMObservation *observation = [[XMObservation alloc] initWithObserver:observer key:keyPath block:block];
// 获取所有观察者组合
NSMutableArray * observations = objc_getAssociatedObject(self, (__bridge const void *)(kXMKVOObservations));
if (!observations) {
observations = [NSMutableArray array];
// 添加关联所有观察者组合
objc_setAssociatedObject(self, (__bridge const void *)(kXMKVOObservations), observations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[observations addObject:observation];
}
// 获取当前类的父类
static Class kvo_class(id self, SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
// 实现 setter 方法
static void kvo_setter(id self, SEL _cmd, id currentValue){
// 根据 setter 获取 getter,_cmd 代表本方法的名称
NSString * setterName = NSStringFromSelector(_cmd);
NSString * getterName = [self getter:setterName];
if (!getterName) {
NSLog(@"没有相应的get方法");
return;
}
// 根据 key 获取对应的旧值
id oldValue = [self valueForKey:getterName];
// 构造 objc_super 的结构体
struct objc_super superclazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
// 对 objc_msgSendSuper 进行类型转换,解决编译器报错的问题
void (* objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
// id objc_msgSendSuper(struct objc_super *super, SEL op, ...) ,传入结构体、方法名称,和参数等
objc_msgSendSuperCasted(&superclazz, _cmd, currentValue);
// 调用之前传入的 block
NSMutableArray * observations = objc_getAssociatedObject(self, (__bridge const void *)(kXMKVOObservations));
for (XMObservation *observation in observations) {
if ([observation.key isEqualToString:getterName]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
observation.block(self, getterName, oldValue, currentValue);
});
}
}
}
// 生成 kvo子类
- (Class)createKVOClassWithOriginalClassName:(NSString *)originalClazzName{
//1.拼接 kvo 子类并生成
NSString * kvoClazzName = [NSString stringWithFormat:@"%@%@",kXMKVOClassPrefix,originalClazzName];
Class kvoClazz =NSClassFromString(kvoClazzName);
//2.如果已经存在则返回
if (kvoClazz) {
return kvoClazz;
}
//3.如果不存在,则传一个父类,类名,然后额外的空间(通常为 0),它返回给你一个子类。
Class originalClazz = object_getClass(self);
kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
//4.重写了 class 方法,隐藏这个新的子类
Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
const char * types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
//5.注册到 runtime 告诉 runtime 这个类的存在
objc_registerClassPair(kvoClazz);
return kvoClazz;
}
//获取 setter 方法字符串
- (NSString *)setter:(NSString *)key{
if (key.length <= 0) {
return nil;
}
// key 第一个大写
NSString * firstStr = [[key substringToIndex:1] uppercaseString];
// 截取 key 第二到最后
NSString * remainingStr = [key substringFromIndex:1];
// 拼接成 setter
NSString * setter = [NSString stringWithFormat:@"set%@%@:", firstStr, remainingStr];
return setter;
}
//获取 getter 方法字符串
- (NSString *)getter:(NSString *)setter{
if (setter.length <=0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
return nil;
}
// 先截掉 set,获取后面属性字符
NSRange range = NSMakeRange(3, setter.length - 4);
NSString * key = [setter substringWithRange:range];
// 把第一个字符换成小写
NSString * firstStr = [[key substringToIndex:1] lowercaseString];
key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstStr];
return key;
}
// 是否包含 selector 方法
- (BOOL)hasSelector:(SEL)selector{
Class clazz = object_getClass(self);
unsigned int methodCount = 0;
// 获取方法列表
Method* methodList = class_copyMethodList(clazz, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
SEL thisSelector = method_getName(methodList[i]);
if (thisSelector == selector) {
free(methodList);
return YES;
}
}
free(methodList);
return NO;
}
4.把这个观察的相关信息存在XMObservation里。观察的相关信息(观察者,被观察的 key, 和传入的 block )封装在XMObservation类里。
@interface XMObservation : NSObject
//观察者
@property (nonatomic, weak) NSObject *observer;
//属性key
@property (nonatomic, copy) NSString *key;
//回调block
@property (nonatomic, copy) XMObservingBlock block;
@end
@implementation XMObservation
- (instancetype)initWithObserver:(NSObject *)observer key:(NSString *)key block:(XMObservingBlock)block {
self = [super init];
if (self) {
_observer = observer;
_key = key;
_block = block;
}
return self;
}
@end
自定义KVO使用
1.创建Dog类,并声明name属性
#import <Foundation/Foundation.h>
@interface Dog : NSObject
@property (nonatomic, copy) NSString * name;
@end
2.添加观察者
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
Dog *dog = [[Dog alloc] init];
_dog = dog;
_dog.name = @"HH";
// 添加观察者
[_dog XM_addObserver:self forKey:@"name" withBlock:^(id observedObject, NSString *observedKey, id oldValue, id currentValue) {
NSLog(@"旧值 = %@ 新值 = %@",oldValue,currentValue);
}];
}
3.点击屏幕,改变属性值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSArray *msgs = @[@"算法导论", @"Objective C", @"Swift", @"操作系统", @"数据结构", @"Java编程", @"编程之美"];
NSUInteger index = arc4random_uniform((u_int32_t)msgs.count);
_dog.name = msgs[index];
}
4.移除观察者
- (void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
//移除观察者
[_dog XM_removeObserver:self forKey:@"name"];
}
5.demo地址