ios KVO的实现原理
首先给大家介绍一下KVO的使用场景:当某个对象的某个属性改变的时候,需要我们做出相应的处理事件。比如我们自定义下拉刷新,那么我们是如何得知用户要进行的下拉刷新数据操作呢,我们可以监听控件的frame,通过用户下拉该控件的时候,会修改该控件的frame.y属性,我们使用KVO监听这个属性。当这个属性的y值在某个范围,我们认为是下拉刷新操作。我们可以去进行数据请求。因为现在下拉刷新的第三方框架有很多,所以很少有人关注下拉刷新的实现原理。 又比如,我们经常使用的网络请求的第三方AFN,它内部监听网络下载进度也是通过KVO实现的。无凭无据,我们来看代码
。
fractionCompleted则是self.downloadProgress对象代表下载进度的属性
等等。所以KVO的使用之处还是比较多的。那么,我们就看看KVO实现的原理是什么样子的,如何实现对属性的监听的。
给大家介绍一下官方的回答:当某个类的对象一次被观察时,系统就会在运行时动态的创建该类的一个派生类(子类),在这个派生类重写被观察属性的setter方法,派生类在被重写的setter方法实现真正的通知机制。派生类重写了class方法以欺骗外部调用者它就是起初监听的那个类。然后系统将这个对象的isa指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对setter的调用就会调用重写的setter,从而激活键值通知机制。此外,派生类还重写了dealloc方法来释放资源。
不知道看完这段话的你,头脑还是否清醒。下面我们一句一句来解释:
创建一个Person类,定义一个age属性,然后我们监听age属性
@interface Person : NSObject @property (nonatomic , assign) int age; @end
@implementation ViewController { Person *_p; } - (void)viewDidLoad { [super viewDidLoad]; //创建Person对象 _p = [[Person alloc] init]; //给Person对象的age属性添加监听 [_p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil]; //修改p的age属性 _p.age = 10; } //KVO回调方法, - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"%@对象的%@属性变化了%@",object,keyPath,change); } //控制器销毁的时候 移除KVO - (void)dealloc { [_p removeObserver:self forKeyPath:@"age"]; }
这个时候,会打印输出
这是KVO使用的步骤。
接下来,我们理解一下这段代码。
1.创建一个Person类的实例对象P
2.给对象P的age属性添加键值监听
3.修改对象P的age属性
4.实现KVO的回调方法
5.移除KVO监听
从输出结果来看,我们是可以正常监听到P的age属性的变化的。
根据官方的解释:当某个类的对象一次被观察时,系统就会在运行时动态的创建该类的一个派生类(子类)
那么,我们看一下这到底是一个什么样的子类:
看断点断的地方,打印这个结果的时候我还没有对对象P进行KVO监听。有的人会问,objc_getClass()是什么,objc开头的函数基本都是runtime里面的函数。所以这是动态获取对象的类型。
当对对象P的age属性进行监听以后,对象P的真实类型变为了NSKVONotifying_Person。那么,为什么po _p 还是<Person>类型的呢,这就是‘派生类重写了class方法以欺骗外部调用者它就是起初监听的那个类’。
接下来,我们再一步步理解。既然有了这个子类了,我们再这个子类的setter方法里面动动手脚,是不是就可以出发KVO的回调了,就会输出监听的结果呢?那么,我们如何让Person类型的P对象,去执行NSKVONotifying_Person类里面的setter方法呢?系统将这个对象(P)的isa指针指向这个新诞生的派生类(NSKVONotifying_Person)。不知道大家对isa指针了解多少?OC所有的类都有一个共同的属性,那就是isa。有图有真相:
那么。这个isa指针是干什么用的呢。
isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。
所以,只要我们修改了isa指针指向的类。就会去它指向的类里面寻找setter方法,也就是会去(NSKVONotifying_Person)类里面寻找setter方法,这个时候我们再NSKVONotifying_Person类的setter方法里面做些事情,从而触发KVO的回调方法,也就是
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"%@对象的%@属性变化了%@",object,keyPath,change); }
是不是就成功了,接下来介绍,我们再setter方法里面具体怎么做,才能触发这个方法呢?
#import "Person.h" @implementation Person - (void)test { [self willChangeValueForKey:@"age"]; [self didChangeValueForKey:@"age"]; } @end
给Person类添加一个test方法。这个时候,我修改ViewController里面的代码
@implementation ViewController { Person *_p; } - (void)viewDidLoad { [super viewDidLoad]; //创建Person对象 _p = [[Person alloc] init]; //给Person对象的age属性添加监听 [_p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil]; //调用test方法 [_p test]; } //KVO回调方法, - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"%@对象的%@属性变化了%@",object,keyPath,change); } //控制器销毁的时候 移除KVO - (void)dealloc { [_p removeObserver:self forKeyPath:@"age"]; }
这个时候,运行代码,你会发现也会触发KVO的回调用法。可是我根本没有改变age属性的值。怎么会触发呢。归根究底,问题出现在test的实现方法里面,也就是
[self willChangeValueForKey:@"age"]; [self didChangeValueForKey:@"age"];
这两句代码是触发KVO的回调方法的关键了。所以,我们只要在NSKVONotifying_Person类里面重写setAge方法。就可以实现KVO监听了。这也就是所谓的“在这个派生类重写被观察属性的setter方法,派生类在被重写的setter方法实现真正的通知机制”;
- (void)setAge:(int)age { [self willChangeValueForKey:@"age"]; self.age = age; [self didChangeValueForKey:@"age"]; }
看完这篇文章,如果大家有不理解的地方,可以留言一起讨论。谢谢。