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"];


}

 

看完这篇文章,如果大家有不理解的地方,可以留言一起讨论。谢谢。

 

posted @ 2017-02-02 20:33  卖报的男孩  阅读(664)  评论(0编辑  收藏  举报