一、KVO的概念
KVO(key-value-observing)是 Objective-C 对观察者设计模式的一种实现,是一种十分有趣的回调机制,在某个对象注册监听者后,在被监听的对象发生改变时,对象会发送一个通知给监听者,监听者作出相应的处理。
二、KVO分析
先来看看苹果文档对于KVO的实现描述
Automatic key-value observing is implemented using a technique called isa-swizzling... 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 ..
KVO 的实现依赖于 Objective-C 强大的 Runtime,文档中关于KVO的实现没有作详细的描述,但是我们知道了KVO是利用Runtime实现后,我就在想我们自己能不能模拟实现一个KVO的实现,下面我们来分析下KVO的具体实现:
isa指针:
isa(isa-swizzling)这个指针,isa是一个Class类型的指针,对象的首地址一般是isa变量,同时isa又保存了对象的类对象的首地址。我们通过object_getClass方法来获取这个对象的元类,即是对象的类对象的类型。
isa作用:
isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。
下面我们来看下原生的KVO对象被观察前后的isa指针的变化:
Person *_p = [[Person alloc] init];
NSLog(@"被观察前use runtime to get class: %@", object_getClass(_p));
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"被观察后use runtime to get class: %@", object_getClass(_p));
2018-03-29 11:01:49.754404+0800 iOS—响应式编程[1811:53355] 被观察前use runtime to get class: Person
2018-03-29 11:02:05.413029+0800 iOS—响应式编程[1811:53355] 被观察后use runtime to get class: NSKVONotifying_Person
从上面代码执行中我们可以看出,当_p对象被观察后,_P对象的isa指针指向了NSKVONotifying_Person,这个时候我们就要思考一个问题了,_p对象的指针是什么时候被改变了呢?这时候我们就要明白KVO内部实现原理了。
KVO原理:
Apple 官方使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO使用Runtime创建一个名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,然后为 NSKVONotifying_A 重写观察属性的 setter 方法,并且在A对象被观察后把A的isa指针指向NSKVONotifying_A,调用setter 方法,通知观察对象属性值的更改情况。
总结一句话:动态创建一个子类,添加重写setter方法。
内部具体实现步骤(结合下面代码,代码中第x步对应这里的步骤):
- 当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类NSKVONotifying_A 。
- 为新建的子类NSKVONotifying_A添加一个setter方法,重写了父类被观察属性 keyPath 的 setter 方法。
- 改变被观察对象的 isa 指针,从指向原来的 A 类改为指向新创建的子类 NSKVONotifying_A 类。(isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象,因而在该对象上对 setter 的调用就会调用已重写的 setter)
- 实现子类的setter方法
- 使用Runtime的关联机制,在添加观察对象的时候关联观察者,在setter方法中取出被观察者。
- objc_msgSend Runtime消息发送通知观察的改变
// 新建一个person类
import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
// 新建一个类别:NSObject+KVO
- (void)Jey_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
// 第一步
NSString *oldClass = NSStringFromClass([self class]);
NSString *newClass = [NSString stringWithFormat:@"JEYKVO_%@",oldClass];
const char *className = [newClass UTF8String];
// 定义一个类
Class myclass = objc_allocateClassPair([self class], className, 0);
// 第二步
// 为这个类添加一个set方法,子类重写
class_addMethod(myclass, @selector(setName:), (IMP)setName, "");
// 注册这个类
objc_registerClassPair(myclass);
// 第三步
// 改变isa指针指向,isa指针指向子类
object_setClass(self, myclass);
// 第五步
// 下一步是怎么通知observer的过程
// 关联对象
objc_setAssociatedObject(self, (__bridge const void*)@"objc", observer, OBJC_ASSOCIATION_RETAIN);
}
// 第四步
void setName (id self, SEL _cmd, NSString *newName) {
id classs = [self class];
object_setClass(self, class_getSuperclass([self class]));
objc_msgSend(self, @selector(setName:),newName);
// 取出关联的对象
id observer = objc_getAssociatedObject(self, (__bridge const void*)@"objc");
// 第六步
NSDictionary *dict =@{@"newName":newName};
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, dict, nil);
// 改回类型
object_setClass(self, classs);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// _p = [[Person alloc] init];
// NSLog(@"被观察前use runtime to get class: %@", object_getClass(_p));
//
// [_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
// NSLog(@"被观察后use runtime to get class: %@", object_getClass(_p));
[_p Jey_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int i = 0;
i ++;
_p.name = [NSString stringWithFormat:@"%d",i];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"%@",change);
}
网友评论