KVO的底层原理
提示:阅读本文需要对isa
和superclass
指针非常熟悉,如果你还不是很清楚的话,可以参考我的isa和superclass的总结.
什么是KVO?
KVO全称是Key-Value Observing,俗称“键值监听”,可用于监听某个对象属性值的改变。KVO的本质分析 先看如下代码
#import "ViewController.h"
#import "CLPerson.h"
@interface ViewController ()
@property (nonatomic, strong) CLPerson *person1;
@property (nonatomic, strong) CLPerson *person2;
@end@implementation ViewController- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[CLPerson alloc] init];
self.person1.age = 1;
self.person2 = [[CLPerson alloc] init];
self.person2.age = 2;
//给person1对象添加kvo监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"test context"];
}- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
self.person1.age = 21;
self.person2.age = 22;
}- (void)dealloc
{
[self.person1 removeObserver:self forKeyPath:@"age"];
}//当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"监听到了%@的%@属性值发生了改变 - %@ - %@",object, keyPath, change, context);
}@end
以上是KVO的简单使用过程,我们对
person1
增加了监听,打印结果如下文章图片
由于没有对person2设置监听,所以日志里面看不到有关person2值改变的信息。
我们在
touchesBegan
方法里面简单地改变了
person1
和
person2
的
age
属性值,
self.person1.age = 21;
self.person1.age = 21;
本质就是调用setter方法,
[self.person1 setAge:21];
[self.person1 setAge:21];
,
而且我们知道系统为属性自动生成的set方法(以这里的age属性为例)其实很简单,就是
文章图片
说到这里,我们肯定都会好奇,既然从本质上,
person1
和
person2
都是调用了
setAge
方法,同样的代码同样的步骤,KVO是如何实现对person1的监听的呢?
将代码跑一下,我们可以发现,无论
person1
还是person2
,确实都走了setAge
方法,但是方法是一样的,所以KVO的秘密肯定不在setAge
方法里面。那看来肯定就是在实例对象身上做文章了。我们在调试器中打印一下person1、person2的isa指针
文章图片
可以看出,
person1
加上
KVO
监听之后,它的
isa
指针指向了一个叫
NSKVONotifying_CLPerson
的
class对象
,而没有加监听的
person2
的
isa
则正常指向
CLPerson
。
NSKVONotifying_CLPerson
不是我们创建的类,它是系统在我们使用KVO给某一个对象增加监听是,利用Runtime技术动态新增的一个类,它是对象原来所属类的一个子类 文章图片
我们借助下面两幅图来先了解一下他们的结构关系
文章图片
这是没有添加KVO监听的person2
的对象结构图
我们通过
文章图片
这是添加了KVO监听的person1
的对象结构图
KVO
给person1
增加监听之后,系统在person1
和CLPerson
的class
对象中间,利用runtime
动态创建了一个NSKVONotifying_CLPerson
类对象,然后将person1
的isa
指针指向NSKVONotifying_CLPerson
,并且它实际上是CLPerson
的子类。如上图所示,这个类对象里面,除了重写了setAge
方法,还重写了class
, dealloc
,以及增加了_isKVOA
方法。-
setAge
方法:KVO的核心魔法就在与对这个方法的重写,虽然苹果没有把这部分的实现开源,但是我们还是有办法推断出内部的大概逻辑的,这里我们先直接说结果。在重写的方法中,实际上调用了Foundation框架的一个c函数_NSSetIntValueAndNotify()
,而这个函数主要就做了这么几件事,我们用为代码来理解一下
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
//调用父类(CLPerson)的setAge方法
[self didChangeValueForKey:@"age"];
}- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
因此我们在来走一遍
[person1 setAge:21];
的调用轨迹:- 向
person1
发送setAge
消息 - 通过
person1
的isa
找到NSKVONotifying_CLPerson
的类对象,调用它的setAge
方法。 -
setAge
中,调用_NSSetIntValueAndNotify()
函数 -
_NSSetIntValueAndNotify()
中,先调用[self willChangeValueForKey:@"age"];
,再调用父类(CLPerson
)的setAge
方法[super setAge:age];
,最后调用[self didChangeValueForKey:@"age"];
。 -
[self didChangeValueForKey:@"age"];
方法里面对监听器进行通知,也就是回调它的监听代理方法 - 整个过程结束。
我们在之前添加KVO的代码出加上两段打印
NSLog(@"person1添加kvo监听之前\nperson1-%@\nperson2-%@", object_getClass(_person1),object_getClass(_person2));
//给person1对象添加kvo监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"test context"];
NSLog(@"person1添加kvo监听之后\nperson1-%@\nperson2-%@", object_getClass(_person1),object_getClass(_person2));
文章图片
这个就证明了
NSKVONotifying_CLPerson
是在代码执行过程中动态生成的新类。
同样我们也可以打印一下KVO前后
setAge:
方法的实现是否有变化
NSLog(@"person1添加kvo监听之前\nperson1-%p\nperson2-%p", [self.person1 methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
//给person1对象添加kvo监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"test context"];
NSLog(@"person1添加kvo监听之前\nperson1-%p\nperson2-%p", [self.person1 methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
文章图片
可以看出,添加KVO之后,
person1
的
setAge:
方法实现地址变了。如果要查看方法更具体一点的信息,可以通过
p (IMP)<具体的方法实现地址>
来打印方法信息。
文章图片
如图,如果是正常的方法,打印信息会显示方法所在的具体模块下的具体文件内的的第几行。我们得以验证,添加KVO之后,
person1
的
setAge:
方法确实是调用了
_NSSetIntValueAndNotify()
。
我顺便又想到了一个问题,
NSKVONotifying_CLPerson
这个类的元类对象是什么?那我们来继续打印一下//给person1对象添加kvo监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"test context"];
NSLog(@"class对象\nperson1-%p\nperson2-%p", object_getClass(_person1),object_getClass(_person2));
NSLog(@"meta-class对象\nperson1-%p\nperson2-%p", object_getClass(object_getClass(_person1)), object_getClass(object_getClass(_person2)));
文章图片
我们可以发现,
person1
和
person2
无论是
class
对象还是
meta-class
对象,都是不一样的,因此说明,在添加了KVO之后,
person1
的
isa
所指向的
NSKVONotifying_CLPerson
的这个类,有自己的对应的
class
对象和
meta-class
对象,是一个完整的类。
关于Foundation框架
我们上面介绍了,KVO添加属性监听之后,
person1
的setAge:
方法内部调用了一个Foundation
函数_NSSetIntValueAndNotify ()
。因为Foundation
是苹果提供的一个动态库,除了Foundation
的.h
文件外,我们无法查看其.m
里面的源代码,但是借助一些逆向工具,我们还是可以窥探他的一些内部细节,这里关于逆向工程的话题我们不作展开,总之,通过抽取Foundation
的.framework
文件(也就是编译成010101机器码的二进制动态库),我们可以在它里找到_NSSetIntValueAndNotify ()
方法,同时,还发现有很多相似的方法文章图片
从规律上,我们猜测,根据属性不同的类型,会使用不同的被监听的对象的
setAge
方法会调用不同的
_NSSetXXXValueAndNotify ()
方法来处理对应属性值的变化。
我们把age属性的类型编程
Double
试试。文章图片
确实,我们又发现了一个
_NSSetDoubleValueAndNotify
方法。
上面我们也总结道
_NSSetXXXValueAndNotify
方法的内部逻辑文章图片
我们也来证明一下。
#import "CLPerson.h"@implementation CLPerson
- (void)setAge:(double)age
{
_age = age;
NSLog(@"调用了setAge方法");
}- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"调用了willChangeValueForKey方法");
}- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"开始调用了didChangeValueForKey方法");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey方法调用结束");
}
@end
虽然我们无法修改
NSKVONotifying_CLPerson
的内容, 但是由于CLPerson
是它的父类,我们可以对它加以修改,以上代码中,我们给几个关键方法都加上日志信息,就可以追踪到他们的调用轨迹。运行程序,日志如下文章图片
日志结果清晰显示了
_NSSetXXXValueAndNotify
函数内部的调用逻辑,与我们的结论吻合。
关于KVO子类的一些细节
【KVO的底层原理】
文章图片
我们前面的图例里面,总结了,KVO监听对象所产生的子类里面,除了有
setter
方法,还有
class
、
dealloc
、
_isKVOA
这么几个方法。我们分别来看一下。
首先我们先用runtime来打印一下
NSKVONotifying_CLPerson
的对象方法列表
-(void)printMethodNamesOfClass:(Class)cls
{
//获取方法
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
//用于存放方法名
NSMutableString *methodNames = [NSMutableString string];
//遍历方法
for (int i = 0;
i < count;
i++) {
//获得方法
Method method = methodList[i];
//转换成方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
//拼接方法名
[methodNames appendString:@"\n - "];
[methodNames appendString:methodName];
}
//释放
free(methodList);
//打印结果
NSLog(@"\n%@%@",cls, methodNames);
}
在给
person1
增加了KVO监听之后,就可以调用这个方法进行打印,结果如下文章图片
dealloc
:这个好理解,这是为了在监听结束,对象被销毁的时候,需要做的一些结束处理收尾工作。
class
:这个方法首先我们先来看一下它的返回值//给`person1`对象添加kvo监听 NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew; [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"test context"]; NSLog(@"\nClass of person1 - %@\nClass of person2 - %@",[self.person1 class],[self.person2 class]); NSLog(@"\nISA of person1 - %@\nISA of person2 - %@",object_getClass(self.person1),object_getClass(self.person2));
文章图片
可以看到,[person1 class]
方法返回的是CLPerson
类,如果系统不重写这个方法,那么这个方法返回的应该是NSKVONotifying_CLPerson
,苹果这么设计,其实原因也很简单, 就是不想让使用者知道KVO的细节,屏蔽内部实现,隐藏有关NSKVONotifying_CLPerson
的信息。让使用者感觉不到KVO的存在和影响,只需要专心使用KVO的监听功能就好。不得不感慨一下苹果api在设计细节上的处理。
到这里,KVO底层的相关原理就基本上都呈现出来了。
_isKVOA
:告诉系统使用了KVO。
面试题解答
iOS用什么方式实现对一个对象的KVO?(KVO的本质)
- 利用Runtime API为被监听对象动态生成一个子类,并且让
instance
对象的isa
指向这个新的子类- 在新的子类中重写属性的
setter
方法。当instance
对象属性被修改的时候,该setter
方法被调用- 在上述的
setter
方法里面,会调用Foundation
对象的_NSSetXXXValueAndNotify
函数,该函数内部的主要逻辑是
- 调用
willChangeValueForKey:
- 调用父类(也就是
instance
对象被监听之前,isa
所指向的class
)的setter
方法,进行成员变量赋值- 调用
didChangeValueForKey:
方法,该方法内部会触发监听器(observer
)的监听方法(observeValueForKeyPath: ofObject: change: context:
)
如何手动触发KVO
手动调用willChangeValueForKey:
和didChangeValueForKey:
即可
直接修改成员变量会触发KVO吗?
触发KVO的条件是通过属性值修改,触发了setter
方法,从而触发KVO回调方法,因此直接修改属性对应的成员变量值,不会触发KVO。
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量