KVO的底层原理

作者: RUNNING_NIUER | 来源:发表于2019-03-22 21:23 被阅读137次

    提示:阅读本文需要对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方法里面。那看来肯定就是在实例对象身上做文章了。

    我们在调试器中打印一下person1、person2的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。

    相关文章

      网友评论

        本文标题:KVO的底层原理

        本文链接:https://www.haomeiwen.com/subject/gqafvqtx.html