KVO

作者: 紫荆秋雪_文 | 来源:发表于2018-07-01 14:01 被阅读2次

    一、KVO介绍

    KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变

    二、KVO实例

    通过一个简单的例子来演示一下KVO,首先创建一个RevanPerson类,并且定义一个age属性,我们通过KVO来监听age值的改变

    • 测试代码
    /*************** RevanPerson.h ***************/
    #import <Foundation/Foundation.h>
    
    @interface RevanPerson : NSObject
    @property (nonatomic, assign) NSInteger age;
    
    @end
    
    /*************** RevanPerson.m ***************/
    #import "RevanPerson.h"
    
    @implementation RevanPerson
    
    @end
    
    /************* KVO测试代码 ***************/
    #import "ViewController.h"
    #import "RevanPerson.h"
    
    @interface ViewController ()
    @property (nonatomic, strong) RevanPerson *revanP1;
    
    @end
    
    @implementation ViewController
    
    - (void)dealloc {
        [self.revanP1 removeObserver:self forKeyPath:@"age"];
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //revanP1
        self.revanP1 = [[RevanPerson alloc] init];
        self.revanP1.age = 11;
        //策略
        NSKeyValueObservingOptions options =  NSKeyValueObservingOptionNew |
        NSKeyValueObservingOptionOld;
        //给self.revanP1添加一个监听者:self
        [self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.revanP1.age = 12;
    }
    
    #pragma mark - KVO的监听方法
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
              ,keyPath
              ,object
              ,change);
    }
    
    @end
    
    当点击屏幕会触发touchesBegan:withEvent:方法来修改age的大小,由于age值的改变会触发KVO的
    监听方法observeValueForKeyPath:ofObject:change:
    输出如下:
    keyPath=age
    object=<RevanPerson: 0x60400000fa20>
    change={
        kind = 1;
        new = 12;//age改变后的新值
        old = 11;  //age改变前的旧值
    }
    

    三、KVO原理分析

    1、为了分析KVO原理,重新创建一个revanP2实例,但是不给revanP2实例添加KVO

    • 测试源码
    /*************** RevanPerson.h ***************/
    #import <Foundation/Foundation.h>
    
    @interface RevanPerson : NSObject
    @property (nonatomic, assign) NSInteger age;
    
    @end
    /*************** RevanPerson.m ***************/
    #import "RevanPerson.h"
    
    @implementation RevanPerson
    
    @end
    
    /************* KVO测试代码 ***************/
    #import "ViewController.h"
    #import "RevanPerson.h"
    
    @interface ViewController ()
    @property (nonatomic, strong) RevanPerson *revanP1;
    @property (nonatomic, strong) RevanPerson *revanP2;
    @end
    
    @implementation ViewController
    
    - (void)dealloc {
        [self.revanP1 removeObserver:self forKeyPath:@"age"];
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //revanP1
        self.revanP1 = [[RevanPerson alloc] init];
        self.revanP1.age = 11;
        
        
        self.revanP2 = [[RevanPerson alloc] init];
        self.revanP2.age = 21;
        //策略
        NSKeyValueObservingOptions options =  NSKeyValueObservingOptionNew |
        NSKeyValueObservingOptionOld;
        //给self.revanP1添加一个监听者:self
        [self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.revanP1.age = 12;
        self.revanP2.age = 22;
    }
    
    #pragma mark - KVO的监听方法
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
              ,keyPath
              ,object
              ,change);
    }
    
    打印输出:只有revanP1的输出
    keyPath=age
    object=<RevanPerson: 0x600000014cb0>
    change={
        kind = 1;
        new = 12;
        old = 11;
    }
    
    

    在触发touchesBegan:withEvent:方法时会给revanP1实例对象和revanP2实例对象的age赋值,会触发age的setAge:方法。Objective-C对象的分类中说过,对象方法信息是存储在class对象中的,类方法信息是存储在meta-class对象中的。所以我们先查看一下revanP1、revanP2的class对象

    • 测试源码
    /************* KVO测试代码 ***************/
    #import "ViewController.h"
    #import "RevanPerson.h"
    #import <objc/runtime.h>
    
    @interface ViewController ()
    @property (nonatomic, strong) RevanPerson *revanP1;
    @property (nonatomic, strong) RevanPerson *revanP2;
    @end
    
    @implementation ViewController
    
    - (void)dealloc {
        [self.revanP1 removeObserver:self forKeyPath:@"age"];
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //revanP1
        self.revanP1 = [[RevanPerson alloc] init];
        self.revanP1.age = 11;
        
        
        self.revanP2 = [[RevanPerson alloc] init];
        self.revanP2.age = 21;
        
        Class revanP1_class = object_getClass(self.revanP1);
        Class revanP2_class = object_getClass(self.revanP2);
        NSLog(@"\nrevanP1KVO之前的class对象:%p-%@", revanP1_class, revanP1_class);
        NSLog(@"\nrevanP2的class对象:%p-%@", revanP2_class, revanP2_class);
        //策略
        NSKeyValueObservingOptions options =  NSKeyValueObservingOptionNew |
        NSKeyValueObservingOptionOld;
        //给self.revanP1添加一个监听者:self
        [self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
        Class revanP1_class_kvo = object_getClass(self.revanP1);
        NSLog(@"\nrevanP1KVO之后的class对象:%p-%@", revanP1_class_kvo, revanP1_class_kvo);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.revanP1.age = 12;
        self.revanP2.age = 22;
    }
    
    #pragma mark - KVO的监听方法
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
              ,keyPath
              ,object
              ,change);
    }
    
    @end
    
    打印输出:
    revanP1KVO之前的class对象:0x10b0dc0b8-RevanPerson
    revanP2的class对象:0x10b0dc0b8-RevanPerson
    revanP1KVO之后的class对象:0x604000116140-NSKVONotifying_RevanPerson
    
    • 在revanP1添加KVO之前,revanP1实例对象和revanP2实例对象的class对象都是是RevanPerson,在revanP1添加了KVO之后revan_P1实例对象的class对象是NSKVONotifying_RevanPerson。
    • 既然revanP1对象的class对象是NSKVONotifying_RevanPerson,那为什么在给revanP1对象的age重新赋值的时候还可以改变age的值呢?NSKVONotifying_RevanPerson中有age这个属性?我们猜测NSKVONotifying_RevanPerson的父类是RevanPerson类
    /************* KVO测试代码 ***************/
    #import "ViewController.h"
    #import "RevanPerson.h"
    #import <objc/runtime.h>
    
    @interface ViewController ()
    @property (nonatomic, strong) RevanPerson *revanP1;
    @property (nonatomic, strong) RevanPerson *revanP2;
    @end
    
    @implementation ViewController
    
    - (void)dealloc {
        [self.revanP1 removeObserver:self forKeyPath:@"age"];
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //revanP1
        self.revanP1 = [[RevanPerson alloc] init];
        self.revanP1.age = 11;
        
        
        self.revanP2 = [[RevanPerson alloc] init];
        self.revanP2.age = 21;
        
        Class revanP1_class = object_getClass(self.revanP1);
        Class revanP2_class = object_getClass(self.revanP2);
        NSLog(@"\nrevanP1KVO之前的class对象:%p-%@", revanP1_class, revanP1_class);
        NSLog(@"\nrevanP2的class对象:%p-%@", revanP2_class, revanP2_class);
        //策略
        NSKeyValueObservingOptions options =  NSKeyValueObservingOptionNew |
        NSKeyValueObservingOptionOld;
        //给self.revanP1添加一个监听者:self
        [self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
        Class revanP1_class_kvo = object_getClass(self.revanP1);
        NSLog(@"\nrevanP1KVO之后的class对象:%p-%@", revanP1_class_kvo, revanP1_class_kvo);
        Class revanP1_superclass_kvo = [revanP1_class_kvo superclass];
        NSLog(@"\nrevanP1KVO之后的superclass对象:%p-%@", revanP1_superclass_kvo, revanP1_superclass_kvo);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.revanP1.age = 12;
        self.revanP2.age = 22;
    }
    
    #pragma mark - KVO的监听方法
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
              ,keyPath
              ,object
              ,change);
    }
    
    @end
    
    打印输出:
    revanP1KVO之前的class对象:0x1074dd0e0-RevanPerson
    revanP2的class对象:0x1074dd0e0-RevanPerson
    revanP1KVO之后的class对象:0x60000011c170-NSKVONotifying_RevanPerson
    revanP1KVO之后的superclass对象:0x1074dd0e0-RevanPerson
    
    
    • 通过代码测试后我们发现NSKVONotifying_RevanPerson这个类是RevanPerson类的子类。

    2、NSKVONotifying_RevanPerson分析

    在revanP1添加KVO的时候,系统会通过runtime动态的创建一个NSKVONotifying_RevanPerson类,并且把revanP1的isa指针指向NSKVONotifying_RevanPerson的class对象。当在给age赋值的时候其实是调用了age的setAge:方法,revanP2会直接调用RevanPerson中的setAge:方法,假设NSKVONotifying_RevanPerson类中没有重写setAge:方法,那么给revanP1.age赋值和给revanP2.age赋值的效果是一样的了,但是实际的情况是没有添加KVO的revanP2在给age赋值的时候是不会触发observeValueForKeyPath:ofObject:change:监听方法,所以猜测NSKVONotifying_RevanPerson类中重写了age的setAge:方法

    /************* KVO测试代码 ***************/
    #import "ViewController.h"
    #import "RevanPerson.h"
    #import <objc/runtime.h>
    
    @interface ViewController ()
    @property (nonatomic, strong) RevanPerson *revanP1;
    @property (nonatomic, strong) RevanPerson *revanP2;
    @end
    
    @implementation ViewController
    
    - (void)dealloc {
        [self.revanP1 removeObserver:self forKeyPath:@"age"];
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //revanP1
        self.revanP1 = [[RevanPerson alloc] init];
        self.revanP1.age = 11;
        
        
        self.revanP2 = [[RevanPerson alloc] init];
        self.revanP2.age = 21;
        
        Class revanP1_class = object_getClass(self.revanP1);
        Class revanP2_class = object_getClass(self.revanP2);
        IMP revanP1_imp = [self.revanP1 methodForSelector:@selector(setAge:)];
        IMP revanP2_imp = [self.revanP1 methodForSelector:@selector(setAge:)];
    
    //    NSLog(@"\nrevanP1KVO之前的class对象:%p-%@", revanP1_class, revanP1_class);
    //    NSLog(@"\nrevanP2的class对象:%p-%@", revanP2_class, revanP2_class);
        NSLog(@"\nrevanP1KVO之前setAge:地址:%p", revanP1_imp);
        NSLog(@"\nrevanP2的setAge:地址:%p", revanP2_imp);
        
        
        //策略
        NSKeyValueObservingOptions options =  NSKeyValueObservingOptionNew |
        NSKeyValueObservingOptionOld;
        //给self.revanP1添加一个监听者:self
        [self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
        
        
        Class revanP1_class_kvo = object_getClass(self.revanP1);
        Class revanP1_superclass_kvo = [revanP1_class_kvo superclass];
        IMP revanP1_imp_kvo = [self.revanP1 methodForSelector:@selector(setAge:)];
        
    //    NSLog(@"\nrevanP1KVO之后的class对象:%p-%@", revanP1_class_kvo, revanP1_class_kvo);
    //    NSLog(@"\nrevanP1KVO之后的superclass对象:%p-%@", revanP1_superclass_kvo, revanP1_superclass_kvo);
        NSLog(@"\nrevanP1KVO之后setAge:地址:%p", revanP1_imp_kvo);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.revanP1.age = 12;
        self.revanP2.age = 22;
    }
    
    #pragma mark - KVO的监听方法
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
              ,keyPath
              ,object
              ,change);
    }
    
    @end
    打印输出:
    revanP1KVO之前setAge:地址:0x1021317c0
    revanP2的setAge:地址:0x1021317c0
    revanP1KVO之后setAge:地址:0x102477bf4
    
    • 在revanP1添加KVO之前,revanP1和revanP2在给age赋值的时候调用的方法是同一个方法。在revanP1添加KVO之后,revanP1在给age赋值时调用了另一个方法。这就说明在NSKVONotifying_RevanPerson中重写了父类的setAge:方法
    • 查看revanP1和revanP2在给age赋值的时候各自调用了什么方法。revanP2调用了setAge:方法而revanP1调用了Foundation中的_NSSetLongLongValueAndNotify函数 查看调用的方法.png
    • 查看对象方法
    /*************** RevanPerson.h ***************/
    #import <Foundation/Foundation.h>
    
    @interface RevanPerson : NSObject
    @property (nonatomic, assign) NSInteger age;
    
    @end
    
    /*************** RevanPerson.m ***************/
    #import "RevanPerson.h"
    
    @implementation RevanPerson
    
    @end
    
    /************* KVO测试代码 ***************/
    #import "ViewController.h"
    #import "RevanPerson.h"
    #import <objc/runtime.h>
    
    @interface ViewController ()
    @property (nonatomic, strong) RevanPerson *revanP1;
    @property (nonatomic, strong) RevanPerson *revanP2;
    @end
    
    @implementation ViewController
    
    - (void)dealloc {
        [self.revanP1 removeObserver:self forKeyPath:@"age"];
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //revanP1
        self.revanP1 = [[RevanPerson alloc] init];
        self.revanP1.age = 11;
        
        self.revanP2 = [[RevanPerson alloc] init];
        self.revanP2.age = 21;
        
        Class revanP1_class = object_getClass(self.revanP1);
        Class revanP2_class = object_getClass(self.revanP2);
        NSLog(@"KVO之前revanP1,class对象中对象方法信息");
        [self methodNamesOfClass:revanP1_class];
        NSLog(@"KVO之前revanP2,class对象中对象方法信息");
        [self methodNamesOfClass:revanP2_class];
        
        //策略
        NSKeyValueObservingOptions options =  NSKeyValueObservingOptionNew |
        NSKeyValueObservingOptionOld;
        //给self.revanP1添加一个监听者:self
        [self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
        
        Class revanP1_class_kvo = object_getClass(self.revanP1);
        NSLog(@"KVO之后revanP1,class对象中对象方法信息");
        [self methodNamesOfClass:revanP1_class_kvo];
        NSLog(@"KVO之后revanP2,class对象中对象方法信息");
        [self methodNamesOfClass:revanP2_class];
    
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.revanP1.age = 12;
        self.revanP2.age = 22;
    }
    
    #pragma mark - KVO的监听方法
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
              ,keyPath
              ,object
              ,change);
    }
    
    - (void)methodNamesOfClass:(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);
    }
    
    @end
    
    打印输出:
    KVO之前revanP1,class对象中对象方法信息:
    RevanPerson setAge:, age,
    KVO之前revanP2,class对象中对象方法信息:
    RevanPerson setAge:, age,
    KVO之后revanP1,class对象中对象方法信息:
    NSKVONotifying_RevanPerson setAge:, class, dealloc, _isKVOA,
    KVO之后revanP2,class对象中对象方法信息:
    RevanPerson setAge:, age,
    
    • revanP2没有添加KVO RevanPersoninstance对象.png
    • revanP1添加KVO KVO底层原理.png

    四、小结

    • KVO本质
      • 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指针指向这个全新的子类
      • 当修改instance对象的属性时,全新的子类会重写属性的set方法,会调用Foundation的_NSSetXXXValueAndNotify函数
        • willChangeValueForKey:
        • 父类原来的setter方法
        • didChangeValueForKey:
        • 内部会触发监听器的监听方法observeValueForKeyPath:ofObject:change:context:
    • 如何手动触发KVO
      • 手动调用willChangeValueForKey:和didChangeValueForKey:
    • 一个属性能不能使用KVO来监听,取决于是否可以重写该属性的set方法

    相关文章

      网友评论

          本文标题:KVO

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