美文网首页
KVO底层原理

KVO底层原理

作者: Goose的小黄花 | 来源:发表于2019-06-27 16:33 被阅读0次

    KVO的全称是Key-Value Observing,俗称键值监听,可以用于监听某个对象属性值的改变。
    下面我们来了解一下KVO的基本使用。

    @interface Person : NSObject
    @property (nonatomic, assign) int age;
    @end
    
    @interface ViewController ()
    @property (nonatomic, strong) Person *person1;
    @property (nonatomic, strong) Person *person2;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[Person alloc] init];
        self.person1.age = 1;
        
        self.person2 = [[Person alloc] init];
        self.person2.age = 2;
        
        //给person对象添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        //    [self.person2 addObserver:self forKeyPath:@"age" options:options context:@"456"];
    }
    
    // 当监听对象的属性值发生改变时,就会调用
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",object,keyPath,change,context);
    }
    
    //移除监听者
    -(void)dealloc{
        [self.person1 removeObserver:self forKeyPath:@"age"];
    //    [self.person2 removeObserver:self forKeyPath:@"age"];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        self.person1.age = 20;
        self.person2.age = 30;
    }
    
    @end
    
    打印结果
    监听到<Person: 0x600003f74540>的age属性值改变了 - {
        kind = 1;
        new = 20;
        old = 1;
    } - 123
    

    上述代码中,我们为person1的对象的age属性添加了观察者,实现了- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法,当person1的age属性值发生变化时,在该方法中监听到我们属性值的变化。而person2没有添加观察者,属性值发生变化时,没有监听到。

    探寻KVO底层实现原理

    通过上述代码我们发现,一旦age属性值发生了改变,就会通知观察者,并且我们知道赋值操作都是调用了属性的set方法。我们到Person类中重写了age的set方法,观察是否是KVO在set方法内部做了一些操作。

    结果是person1和person2对象调用同样的set方法,但person1对象除了调用set方法之外还会执行观察者的observeValueForKeyPath方法。同样是调用了setAge方法,那为什么结果不一样呢?在调用方法一致的情况下,差异应该是出现在对象本身上,person1对象本身在底层肯定发生了变化,KVO应该是对person1做了一些不为人知的事情。下面我们就去探索一下究竟发生了什么变化。

    本质分析

    首先我们在上述代码touchedBegan方法中,给age赋值的代码打上断点,观察一下person1在addObserver后,发生了什么改变,我们通过打印isa指针,如下图所示

    图1.addObserver后person1对象变化

    通过上图我们发现,person1对象执行addObserver操作之后,person1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifying_Person类对象,并且NSKVONotifying_Person类的superclass指针指向Person类,说明NSKVONotifying_Person类为Person类的子类,而person2对象没有发生任何改变。也就是说一旦person1对象添加KVO监听之后,其isa指针就会发生变化,因此set方法的执行效果就不一样了。

    我们来看两张图,分别代表person2和person1在内存中存储的情况,用来做对比。


    图2.未使用KVO监听的对象
    图3.使用了KVO监听的对象

    首先分析未使用KVO的对象person2,其isa指向的是类对象Person,Person中保存着person2的set和get方法,当对person2的age赋值时,直接找到Person类中保存的setAge方法进行调用,不会监听到值的变化。
    而使用了的KVO的person1对象,其isa指向了一个新的类NSKVONotifying_Person,新的类中也创建了setAge方法。当对person1的age赋值时,就找到NSKVONotifying_Person类中的setAge方法并调用,监听值变化是在这个方法中实现的。系统通过runtime动态的创建了NSKVONotifying_Person类,并且NSKVONotifying_Person类继承至Person类,这就是为什么person1的isa指向新的类之后,我们之前在Person中重写了set方法也同样会调用。

    接下来我们只需要弄清楚,NSKVONotifying_Person类中的setAge方法具体做了哪些事情。
    进过分析,NSKVONotifying_Person中的setAge方法中其实调用了 Fundation框架中C语言函数 _NSsetIntValueAndNotify,主要是以下流程,用伪代码来做一下说明。

    • 调用willChangeValueForKey:将要改变方法
    • 调用父类的setAge方法对成员变量赋值
    • 调用didChangeValueForKey:已经改变方法,didChangeValueForKey:中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath:ofObject:change:context方法中
    // 以下都为伪代码
    - (void)setAge:(int)age
    {
        _NSSetIntValueAndNotify();
    }
    
    void _NSSetIntValueAndNotify()
    {
        [self willChangeValueForKey:@"age"];
        [super setAge:age];
        [self didChangeValueForKey:@"age"];
    }
    
    - (void)didChangeValueForKey:(NSString *)key
    {
        // 通知监听器,某某属性值发生了改变
        [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
    }
    
    本质验证

    在最开始给的实例代码中,给person1对象添加观察者之前之后分别打印person1和person2的setAge方法对应的地址。

    NSLog(@"person1添加KVO监听之前 - %p %p",
            [self.person1 methodForSelector:@selector(setAge:)],
            [self.person2 methodForSelector:@selector(setAge:)]);
        
        //给person1对象添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
        NSLog(@"person1添加KVO监听之后 - %p %p",
              [self.person1 methodForSelector:@selector(setAge:)],
              [self.person2 methodForSelector:@selector(setAge:)]);
    
    打印结果
    2019-06-26 17:09:00.276025+0800 KVO原理[90398:25370931] person1添加KVO监听之前 - 0x10f5a7530 0x10f5a7530
    2019-06-26 17:09:02.655540+0800 KVO原理[90398:25370931] person1添加KVO监听之后 - 0x10f9023d2 0x10f5a7530
    (lldb) p IMP(0x10f5a7530)
    (IMP) $0 = 0x000000010f5a7530 (KVO原理`-[Person setAge:] at Person.m:13)
    (lldb) p IMP(0x10f9023d2)
    (IMP) $1 = 0x000000010f9023d2 (Foundation`_NSSetIntValueAndNotify)
    (lldb) 
    

    添加观察者之前,两个setAge方法对应的地址是一样的,添加观察者之后person1的setAge方法地址发生了变化。打印person2的setAge方法,显示的是调用Person类中的方法;打印person1的setAge方法,此时显示是调用了Foundation中的C语言函数_NSSetIntValueAndNotify

    Foundation框架会根据属性的类型,调用不同的方法。例如我们之前定义的int类型的age属性,调用的是_NSSetIntValueAndNotify函数,那我们吧age的属性类型变为double再重新打印一遍。

    打印结果
    2019-06-26 17:42:41.731276+0800 KVO原理[90733:25452474] person1添加KVO监听之前 - 0x1023e0500 0x1023e0500
    2019-06-26 17:42:42.430932+0800 KVO原理[90733:25452474] person1添加KVO监听之后 - 0x10273b18c 0x1023e0500
    (lldb) p IMP(0x1023e0500)
    (IMP) $0 = 0x00000001023e0500 (KVO原理`-[Person setAge:] at Person.m:13)
    (lldb) p IMP(0x10273b18c)
    (IMP) $1 = 0x000000010273b18c (Foundation`_NSSetDoubleValueAndNotify)
    (lldb) 
    

    我们发现调用的函数变为了_NSSetDoubleValueAndNotify,那么我们猜测Foundation框架中应该有许多地类型的函数,通过不同的类型调用不同的函数。
    通过一些反编译手段,我们可以找到Foundation框架文件,通过执行命令行查询关键字找到相关函数。

    图4.查看NSSetValueAndNotify的相关函数

    _NSSetIntValueAndNotify内部的调用顺序又是怎样的呢?我们继续验证。
    因为不能直接看到Foundation框架中的方法实现,我们可以通过父类中重写相关方法,也能看出方法的调用顺序。

    @interface Person : NSObject
    @property (nonatomic, assign) int age;
    @end
    
    @implementation Person
    
    - (void)setAge:(int)age{
        _age = age;
        NSLog(@"setAge:");
    }
    
    - (void)willChangeValueForKey:(NSString *)key
    {
        [super willChangeValueForKey:key];
        
        NSLog(@"willChangeValueForKey");
    }
    
    - (void)didChangeValueForKey:(NSString *)key
    {
        NSLog(@"didChangeValueForKey - begin");
        
        [super didChangeValueForKey:key];
        
        NSLog(@"didChangeValueForKey - end");
    }
    
    @end
    
    @interface ViewController ()
    @property (nonatomic, strong) Person *person1;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[Person alloc] init];
        self.person1.age = 1;
        
        //给person1对象添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    }
    
    // 当监听对象的属性值发生改变时,就会调用
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",object,keyPath,change,context);
    }
    
    //移除监听者
    -(void)dealloc{
        [self.person1 removeObserver:self forKeyPath:@"age"];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.person1.age = 20;
    }
    
    打印结果
    2019-06-26 18:18:22.365864+0800 KVO原理[91127:25550178] willChangeValueForKey
    2019-06-26 18:18:22.366283+0800 KVO原理[91127:25550178] setAge:
    2019-06-26 18:18:22.366337+0800 KVO原理[91127:25550178] didChangeValueForKey - begin
    2019-06-26 18:18:22.366449+0800 KVO原理[91127:25550178] 监听到<Person: 0x600003ee8d10>的age属性值改变了 - {
        kind = 1;
        new = 20;
        old = 20;
    } - 123
    2019-06-26 18:18:22.366573+0800 KVO原理[91127:25550178] didChangeValueForKey - end
    

    由以上打印结果可以看出,最先调用willChangeValueForKey方法,再调用setAge:方法,最后调用didChangeValueForKey已经改变方法,didChangeValueForKey中会调用监听器的监听方法。

    如何证明NSKVONotifyin_Person内部结构如上图3所示呢?
    我们通过runtime分别打印Person类对象和NSKVONotifying_Person类对象存储的对象方法。

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[MJPerson alloc] init];
        self.person1.age = 1;
        
        self.person2 = [[MJPerson alloc] init];
        self.person2.age = 2;
        
        // 给person1对象添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
        [self printMethodNamesOfClass:object_getClass(self.person1)];
        [self printMethodNamesOfClass:object_getClass(self.person2)];
    }
    
    - (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:methodName];
            [methodNames appendString:@", "];
        }
        
        // 释放
        free(methodList);
        
        // 打印方法名
        NSLog(@"%@ %@", cls, methodNames);
    }
    
    打印结果
    2019-06-27 15:18:33.343736+0800 KVO原理[2714:27649615] Person setAge:, age,
    2019-06-27 15:18:33.343855+0800 KVO原理[2714:27649615] NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,
    

    通过上述代码的打印结果,我们发现NSKVONotifying_Person类中有4个对象方法。分别为setAge:,class, dealloc, _isKVOA。至此可以验证图上所示结构。
    我们分析以下系统为什么要生成这几个方法

    • setAge:方法在上面已经验证过是调用的C语言的_NSSetIntValueAndNotify
    • dealloc方法应该是在内部做一些收尾工作
    • _isKVOA应该就是一个返回值为BOOL类型的函数,当实现KVO就默认返回YES
    • claas方法应该就是为了屏蔽内部实现,隐藏了NSKVONotifying_Person类的存在,直接返回Person类

    我们在person1添加过KVO监听之后,分别打印person1和person2对象的class可以发现他们都返回Person。

    NSLog(@"%@,%@",[person1 class],[person2 class]);
    
    打印结果 Person,Person
    

    如果NSKVONotifying_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到NSObject,而NSObject的class的实现大致为返回自己isa指向的类,返回person1的isa指向的类那么打印出来的类就是NSKVONotifying_Person,但是Apple不希望将NSKVONotifying_Person类暴露出来,并且不希望我们知道NSKVONotifying_Person内部实现,所以在内部重写了class类,直接返回Person类,那么外界在调用person1的class对象方法时,打印是Person类。这样person1给外界的感觉person1还是Person类,并不知道NSKVONotifying_Person子类的存在。

    那么我们可以猜测NSKVONotifying_Person内重写的class内部实现大致为

    - (Class) class {
         // 得到类对象,在找到类对象父类
         return class_getSuperclass(object_getClass(self));
    }
    

    面试题

    1.iOS用什么方法实现对一个对象的KVO?(KVO的本质是什么?)
    利用Runtime动态的生成一个instance对象原本类的子类,并且让instance对象的isa指向这个全新的子类
    当修改instance对象的属性时,会调用Foundation的_NSSet***ValueAndNotify函数

    • willChangeValueForKey:
    • 调用父类原来的set方法
    • didChangeValueForKey:
    • 内部会出发监听器(Observer)的监听方法observeValueForKeyPath:ofObject:change:context

    2.如何手动触发KVO?
    想手动触发KVO,我们需要自己调用willChangeValueForKey:didChangeValueForKey:两个方法,两个方法缺一不可。

    代码验证

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.person1 = [[Person alloc] init];
        self.person1.age = 1;
    
        //给person1对象添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
        [self.person1 willChangeValueForKey:@"age"];
        [self.person1 didChangeValueForKey:@"age"];
        
        [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);
    }
    
    打印结果
    2019-06-27 16:06:59.103817+0800 KVO原理[3176:27785539] 监听到<Person: 0x6000024f87a0>的age属性值改变了 - {
        kind = 1;
        new = 1;
        old = 1;
    } - 123
    

    通过打印我们可以发现,age值没有发生变化的情况下,通过手动调用willChangeValueForKey: 和didChangeValueForKey:方法,内部成功调用了observeValueForKeyPath:ofObject:change:context:方法。

    3.直接修改成员变量的值会触发KVO么?
    因为直接修改成员变量的值不会调用set方法,所以就不会触发KVO

    相关文章

      网友评论

          本文标题:KVO底层原理

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