KVO的底层原理

本文详细探讨了Objective-C中的Key-Value Observing(KVO)原理,包括其本质是通过Runtime动态创建子类来实现监听,以及KVO如何在setter方法中调用私有方法进行通知。通过分析KVO前后对象的isa指针变化,揭示了KVO子类的内部结构和方法重写。此外,还介绍了KVO与Foundation框架的关系以及在面试中可能遇到的相关问题。
摘要由CSDN通过智能技术生成

提示:阅读本文需要对isasuperclass指针非常熟悉,如果你还不是很清楚的话,可以参考我的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<UITouch *> *)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<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到了%@的%@属性值发生了改变 - %@ - %@",object, keyPath, change, context);
}


@end

以上是KVO的简单使用过程,我们对person1增加了监听,打印结果如下
由于没有对person2设置监听,所以日志里面看不到有关person2值改变的信息。
我们在touchesBegan方法里面简单地改变了person1person2age属性值,
self.person1.age = 21;
self.person1.age = 21;
本质就是调用setter方法,
[self.person1 setAge:21];
[self.person1 setAge:21];
而且我们知道系统为属性自动生成的set方法(以这里的age属性为例)其实很简单,就是
说到这里,我们肯定都会好奇,既然从本质上,person1person2都是调用了setAge方法,同样的代码同样的步骤,KVO是如何实现对person1的监听的呢?

将代码跑一下,我们可以发现,无论person1还是person2,确实都走了setAge方法,但是方法是一样的,所以KVO的秘密肯定不在setAge方法里面。那看来肯定就是在实例对象身上做文章了。

我们在调试器中打印一下person1person2的isa指针

可以看出,person1加上KVO监听之后,它的isa指针指向了一个叫NSKVONotifying_CLPersonclass对象,而没有加监听的person2isa则正常指向CLPerson
NSKVONotifying_CLPerson不是我们创建的类,它是系统在我们使用KVO给某一个对象增加监听是,利用Runtime技术动态新增的一个类,它是对象原来所属类的一个子类
我们借助下面两幅图来先了解一下他们的结构关系

这是没有添加KVO监听的person2的对象结构图

这是添加了KVO监听的person1的对象结构图

我们通过KVOperson1增加监听之后,系统在person1CLPersonclass对象中间,利用runtime动态创建了一个NSKVONotifying_CLPerson类对象,然后将person1isa指针指向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];的调用轨迹:

  1. person1发送setAge消息
  2. 通过person1isa找到NSKVONotifying_CLPerson的类对象,调用它的setAge方法。
  3. setAge中,调用_NSSetIntValueAndNotify()函数
  4. _NSSetIntValueAndNotify()中,先调用[self willChangeValueForKey:@"age"];,再调用父类(CLPerson)的setAge方法[super setAge:age];,最后调用[self didChangeValueForKey:@"age"];
  5. [self didChangeValueForKey:@"age"];方法里面对监听器进行通知,也就是回调它的监听代理方法
  6. 整个过程结束。

KVO本质的验证

我们在之前添加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之后,person1setAge:方法实现地址变了。如果要查看方法更具体一点的信息,可以通过p (IMP)<具体的方法实现地址>来打印方法信息。

如图,如果是正常的方法,打印信息会显示方法所在的具体模块下的具体文件内的的第几行。我们得以验证,添加KVO之后,person1setAge:方法确实是调用了_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)));


我们可以发现,person1person2无论是class对象还是meta-class对象,都是不一样的,因此说明,在添加了KVO之后,person1isa所指向的NSKVONotifying_CLPerson的这个类,有自己的对应的class对象和meta-class对象,是一个完整的类。

关于Foundation框架

我们上面介绍了,KVO添加属性监听之后,person1setAge:方法内部调用了一个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监听对象所产生的子类里面,除了有setter方法,还有classdealloc_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在设计细节上的处理。

  • _isKVOA:告诉系统使用了KVO。

到这里,KVO底层的相关原理就基本上都呈现出来了。

面试题解答

iOS用什么方式实现对一个对象的KVO?(KVO的本质)

  • 利用Runtime API为被监听对象动态生成一个子类,并且让instance对象的isa指向这个新的子类
  • 在新的子类中重写属性的setter方法。当instance对象属性被修改的时候,该setter方法被调用
  • 在上述的setter方法里面,会调用Foundation对象的_NSSetXXXValueAndNotify函数,该函数内部的主要逻辑是
    1. 调用willChangeValueForKey:
    2. 调用父类(也就是instance对象被监听之前,isa所指向的class)的setter方法,进行成员变量赋值
    3. 调用didChangeValueForKey:方法,该方法内部会触发监听器(observer)的监听方法(observeValueForKeyPath: ofObject: change: context:

如何手动触发KVO
手动调用willChangeValueForKey:didChangeValueForKey:即可

直接修改成员变量会触发KVO吗?
触发KVO的条件是通过属性值修改,触发了setter方法,从而触发KVO回调方法,因此直接修改属性对应的成员变量值,不会触发KVO。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值