美文网首页
iOS KVO本质探索

iOS KVO本质探索

作者: 木子雨廷t | 来源:发表于2020-01-08 10:37 被阅读0次

    KVO全称为Key Value Observing,键值监听机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,所以NSObject的子类都可使用该方法。

    KVO使用步骤

    1.注册观察者(为被观察这指定观察者以及被观察者属性)
    创建一个Person对象,写一个age属性,为age属性添加KVO监听

    /* 
    options: 有4个值,分别是:
    NSKeyValueObservingOptionOld 把更改之前的值提供给处理方法 
    NSKeyValueObservingOptionNew 把更改之后的值提供给处理方法 
    NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。 
    NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。 
     */
    //注册一个监听器用于监听指定的key路径
    [self.person addObserver:self forKeyPath:@"age" options:options context:@"123"];
    

    2.实现回调方法

    // 当监听对象的属性值发生改变时,就会调用
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    }
    

    3.修改需要监听的属性值,查看是否监听成功

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.person1.age = 10;
    }
    
    使用注意点

    [self.person addObserver:self forKeyPath:@"age" options:options context:@"123"] ;这行代码中context:@"123"这个参数传的值都会
    -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{}传到回调方法中context中来,用途: 如果说在一个控制器中我们监听了多个属性的变化,而每一个属性的变化之后对应的事件处理也是不一样的,那么在注册观察者时context 传不同的字符串就可以区分判断了

    思考:为什么注册观察者之后,修改观察对象属性的值,就会走回调方法?KVO是怎么实现这个功能的呢?
    带着这两个问题下面来一探究竟
    • OC是消失机制当我们调用 self.person1.age = 10; 这行代码时实际就是[self.person setAge:10] 然后再转化成objc_msgsend(person,@selector(setAge)),那么是不是KVO在setAge 这个方法中做了文章呢?前几篇文章已经写到对象方法存储到类对象中,那么在控制台输入 po self.person.isa 发现结果并不是 Person 而是 NSKVONotifying_Person 这说明KVO在运行时新建了一个NSKVONotifying_Person类,将person的isa指针指向这个类, 在控制台输入 po [self.person class]发现输出的是Person而不是NSKVONotifying_Person。这是为什么?

    猜测:系统在运行时,KVO动态创建一个NSKVONotifying_Person类,将person的isa指针指向这个类。新创建的NSKVONotifying_Person 继承与Person 并且重写了 setAge方法和 class方法

    验证1:NSKVONotifying_Person是否继承自Person,是否重写了Class方法
    #import <objc/runtime.h>
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        self.person1.age = 20;
        //获取类对象
        Class personClass = object_getClass(self.person1);
        //通过类对象获取父类
        Class personClass02 = class_getSuperclass(personClass);
        NSLog(@"%@-------%@",personClass02,personClass03);
    }
    输出 NSKVONotifying_Person-------Person
    

    通过这几行代码就可以证明NSKVONotifying_Person 继承自 Person

    • 细节
      再获取类对象时可以使用object_getClass 也可以使用[A class],上面为什么使用了object_getClass而不是使用[A class]呢?
      答案: NSKVONotifying_Person 重写了class方法,[person class]返回的并不是原对象而是原对象的父类也就是Person类,如果没有重写的话,返回person的isa指针指向的类打印结果应该是NSKVONotifyin_Person,但是苹果官方不希望将NSKVONotifyin_Person类的内部实现暴露出来,所以在内部重写了class方法,直接返回Person类,所以我们在调用person的class方法时,返回的是Person类。
    验证2:NSKVONotifying_Person是否重写了setAge方法
      //新建另外一个person2对象,不添加监听
     self.person2 = [[Person alloc] init];
     self.person2.age = 2;
    
    //点击修改person2的值
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.person.age = 20;
        Class personClass = object_getClass(self.person);
        Class personClass02 = object_getClass(personClass);
        Class personClass03 = class_getSuperclass(personClass);
        
        //修改person2的值
        self.person2.age = 20;
    }
    

    答案:person2 在调用setAge方法时并没有触发回调方法,而添加了观察者的person对象在调用setAge 方法时触发了方法,说明NSKVONotifying_Person 重写了setAge方法

    思考1:NSKVONotifying_Person怎么重写了setAge方法以实现触发回调

    经过查看底层源码和相关资料分析们可以知道,NSKVONotifyin_Person中的setAge方法中其实调用了Fundation框架中C语言函数_NSsetIntValueAndNotify,而_NSsetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey方法,之后调用父类的setAge方法对成员变量赋值,最后调用didChangeValueForKey方法。其中didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法。

    思考2:NSKVONotifyin_Person的内部结构是怎样的?

    NSKVONotifyin_Person作为Person的子类,其superclass指针指向Person类,并且NSKVONotifyin_Person内部的setAge方法做了单独的实现。我们可以通过runtime的方法去分别打印person1person2两个对象和NSKVONotifyin_Person类对象内存储的对象方法:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.person1 = [[Person alloc] init];
        self.person1.age = 1;
        
        self.person2 = [[Person alloc] init];
        self.person2.age = 2;
    // 给person1对象添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    
        [self printMethods: object_getClass(self.person1)];
        [self printMethods: object_getClass(self.person2)];
    
        [self.person1 removeObserver:self forKeyPath:@"age"];
    }
    
    - (void) printMethods:(Class)cls
    {
        unsigned int count ;
        Method *methods = class_copyMethodList(cls, &count);
        NSMutableString *methodNames = [NSMutableString string];
        [methodNames appendFormat:@"%@ : ", cls];
        
        for (int i = 0 ; i < count; i++) {
            Method method = methods[I];
            NSString *methodName  = NSStringFromSelector(method_getName(method));
            
            [methodNames appendString: methodName];
            [methodNames appendString:@" "];
            
        }
        
        NSLog(@"%@",methodNames);
        free(methods);
    }
    

    打印输出:

    Person : setAge:, age,
    NSKVONotifying_Person : setAge:, class, dealloc, _isKVOA,
    

    NSKVONotifyin_Person的内存结构及方法调用顺序


    NSKVONotifyin_Person的内存结构及方法调用顺序图解
    总结:

    1、KVO的本质是什么?
    当我们给对象注册一个观察者添加了KVO监听时,系统会修改这个对象的isa指针指向。在运行时,动态创建一个新的子类,NSKVONotifying_A类,将A的isa指针指向这个子类,来重写原来类的set方法;set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
    2、如何手动触发KVO?
    实现调用willChangeValueForKey和didChangeValueForKey方法。

    如有疑问:
    iOS OC对象的本质窥探(一)
    iOS OC对象的本质窥探(对象分类)(二)

    特别推荐:
    iOS获取手机唯一标示
    iOS 高德地图实现大头针展示,分级大头针,自定制大头针,在地图上画线,线和点共存,路线规划(驾车路线规划),路线导航,等一些常见的使用场景

    相关文章

      网友评论

          本文标题:iOS KVO本质探索

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