zoukankan      html  css  js  c++  java
  • 【OC底层】KVO原理

    KVO的原理是什么?底层是如何实现的?

    KVO是Key-value observing的缩写。

    KVO是Objective-C是使用观察者设计模式实现的。

    Apple使用了isa混写(isa-swizzling)来实现KVO。

    我们可以通过代码去探索一下。

    创建自定义类:XGPerson

    @interface XGPerson : NSObject
    
    @property (nonatomic,assign) int age;
    
    @property (nonatomic,copy) NSString* name;
    
    @end

    我们的思路就是看看对象添加KVO之前和之后有什么变化,是否有区别,代码如下:

    @interface ViewController ()
    
    @property (strong, nonatomic) XGPerson *person1;
    @property (strong, nonatomic) XGPerson *person2;
    
    @end
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[XGPerson alloc]init];
        self.person2 = [[XGPerson alloc]init];
        self.person1.age = 1;
        self.person2.age = 10;
    
        // 添加监听之前,获取类对象,通过两种方式分别获取 p1 和 p2的类对象
        NSLog(@"before getClass--->> p1:%@  p2:%@",object_getClass(self.person1),object_getClass(self.person2));
        NSLog(@"before class--->> p1:%@  p2:%@",[self.person1 class],[self.person2 class]);
            
        // 添加KVO监听
        NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];
    
        // 添加监听之后,获取类对象
        NSLog(@"after getClass--->> p1:%@  p2:%@",object_getClass(self.person1),object_getClass(self.person2));
        NSLog(@"after class--->> p1:%@  p2:%@",[self.person1 class],[self.person2 class]);
    }

    输出:

    2018-11-02 15:16:13.276167+0800 KVO原理[4083:170379] before getClass--->> p1:XGPerson  p2:XGPerson
    2018-11-02 15:16:13.276271+0800 KVO原理[4083:170379] before class--->> p1:XGPerson  p2:XGPerson
    
    
    2018-11-02 15:16:13.276712+0800 KVO原理[4083:170379] after getClass--->> p1:NSKVONotifying_XGPerson  p2:XGPerson
    2018-11-02 15:16:13.276815+0800 KVO原理[4083:170379] after class--->> p1:XGPerson  p2:XGPerson

    从上面可以看出,object_getClass 和 class 方式分别获取到的 类对象竟然不一样,在对象添加了KVO之后,使用object_getClass的方式获取到的对象和我们自定义的对象不一样,而是NSKVONotifying_XGPerson,可以怀疑 class 方法可能被篡改了.

    最终发现NSKVONotifying_XGPerson是使用Runtime动态创建的一个类,是XGPerson的子类.

    看完对象,接下来我们来看下属性,就是被我们添加了KVO的属性age,我们要触发KVO回调就是去给age设置个值,那它肯定就是调用setAge这个方法.

    下面监听下这个方法在被添加了KVO之后有什么不一样.

        NSLog(@"person1添加KVO监听之前 - %p %p",
                  [self.person1 methodForSelector:@selector(setAge:)],
                  [self.person2 methodForSelector:@selector(setAge:)]);
    
    
        // 添加KVO监听
        NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];
    
        NSLog(@"person1添加KVO监听之后 - %p %p",
              [self.person1 methodForSelector:@selector(setAge:)],
              [self.person2 methodForSelector:@selector(setAge:)]);

    输出:

    2018-11-02 15:16:13.276402+0800 KVO原理[4083:170379] person1添加KVO监听之前 - 0x10277c3e0 0x10277c3e0
    
    2018-11-02 15:16:17.031319+0800 KVO原理[4083:170379] person1添加KVO监听之后 - 0x102b21f8e 0x10277c3e0

    看输出我们能发现,在监听之前两个对象的方法所指向的物理地址都是一样的,添加监听后,person1对象的setAge方法就变了,这就说明一个问题,这个方法的实现变了,我们再通过Xcode断点调试打印看下到底调用什么方法

    断点后,在调试器中使用 po 打印对象

    (lldb) po [self.person1 methodForSelector:@selector(setAge:)]

      (Foundation`_NSSetIntValueAndNotify)

     

    (lldb) po [self.person2 methodForSelector:@selector(setAge:)]

      (KVO原理`-[XGPerson setAge:] at XGPerson.m:13)

    通过输出结果可以发现person1的setAge已经被重写了,改成了调用Foundation框架中C语言写的 _NSSetIntValueAndNotify 方法,

    还有一点,监听的属性值类型不同,调用的方法也不同,如果是NSString的,就会调用 _NSSetObjectValueAndNotify 方法,会有几种类型

    大家都知道苹果的代码是不开源的,所以我们也不知道 _NSSetIntValueAndNotify 这个方法里面到底调用了些什么,那我们可以试着通过其它的方式去猜一下里面是怎么调用的。

    KVO底层的调用顺序

    我们先对我们自定义的类下手,重写下类里面的几个方法:

    类实现:

    #import "XGPerson.h"
    
    @implementation XGPerson
    
    - (void)setAge:(int)age{
        
        _age = age;
        NSLog(@"XGPerson setAge");
    }
    
    - (void)willChangeValueForKey:(NSString *)key{
        
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey");
    }
    
    - (void)didChangeValueForKey:(NSString *)key{
        
        NSLog(@"didChangeValueForKey - begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey - end");
    }

    重写上面3个方法来监听我们的值到底是怎么被改的,KVO的通知回调又是什么时候调用的

    我们先设置KVO的监听回调

    // KVO监听回调
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        
        NSLog(@"监听到%@的%@属性值改变了 - %@", object, keyPath, change[@"new"]);
    }

    我们直接修改person1的age值,触发一下KVO,输出如下:

    2018-11-02 15:38:24.788395+0800 KVO原理[4298:186471] willChangeValueForKey
    2018-11-02 15:38:24.788573+0800 KVO原理[4298:186471] XGPerson setAge
    2018-11-02 15:38:24.788696+0800 KVO原理[4298:186471] didChangeValueForKey - begin
    2018-11-02 15:38:24.788893+0800 KVO原理[4298:186471] 监听到<XGPerson: 0x60400022f420>的age属性值改变了 - 2
    2018-11-02 15:38:24.789014+0800 KVO原理[4298:186471] didChangeValueForKey - end

    从结果中可以看出KVO是在哪个时候触发回调的,就是在 didChangeValueForKey 这个方法里面触发的

    NSKVONotifying_XGPerson子类的研究

    接下来我们再来研究下之前上面说的那个 NSKVONotifying_XGPerson 子类,可能大家会很好奇这里面到底有些什么东西,下面我们就使用runtime将这个子类的所有方法都打印出来

    我们先写一个方法用来打印一个类对象的所有方法,代码如下:

    // 获取一个对象的所有方法
    - (void)getMehtodsOfClass:(Class)cls{
        
        unsigned int count;
        Method* methods = class_copyMethodList(cls, &count);
        
        NSMutableString* methodList = [[NSMutableString alloc]init];
        for (int i=0; i < count; i++) {
            Method method = methods[i];
            NSString* methodName = NSStringFromSelector(method_getName(method));
            [methodList appendString:[NSString stringWithFormat:@"| %@",methodName]];
        }
        NSLog(@"%@对象-所有方法:%@",cls,methodList);

       // C语言的函数是需要手动释放内存的喔
       free(methods);

    }

    下面使用这个方法打印下person1的所有方法,顺便我们再对比下 object_getClass 和 class

        // 一定要使用 object_getClass去获取类对象,不然获取到的不是真正的那个子类,而是XGPperson这个类
        [self getMehtodsOfClass:object_getClass(self.person1)];

       // 使用 class属性获取的类对象 [self getMehtodsOfClass:[self.person1 class]];

    输出:

    2018-11-02 15:45:07.918209+0800 KVO原理[4369:190437] NSKVONotifying_XGPerson对象-所有方法:| setAge:| class| dealloc| _isKVOA
    2018-11-02 15:45:07.918371+0800 KVO原理[4369:190437] XGPerson对象-所有方法:| .cxx_destruct| name| willChangeValueForKey:| didChangeValueForKey:| setName:| setAge:| age

    通过结果可以看出,这个子类里面就是重写了3个父类方法,还有一个私有的方法,我们XGPerson这个类还有一个name属性,这里为什么没有setName呢?因为我们没有给 name 属性添加KVO,所以就不会重写它,这里面确实有那个 class 方法,确实被重写了,所以当我们使用 [self.person1 class] 的方式的时候它内部怎么返回的就清楚了。

    NSKVONotifying_XGPerson 伪代码实现

    通过上面的研究,我们大概也能清楚NSKVONotifying_XGPerson这个子类里面是如何实现的了,大概的代码如下:

    头文件:

    @interface NSKVONotifying_XGPerson : XGPerson
    
    @end

    实现:

    #import "NSKVONotifying_XGPerson.h"
    
    // KVO的原理伪代码实现
    @implementation NSKVONotifying_XGPerson
    
    - (void)setAge:(int)age{
        
        _NSSetIntValueAndNotify();
    }
    
    - (void)_NSSetIntValueAndNotify{
        
        // KVO的调用顺序
        [self willChangeValueForKey:@"age"];
        [super setAge:age];
        // KVO会在didChangeValueForKey里面调用age属性变更的通知回调
        [self didChangeValueForKey:@"age"];
    }
    
    - (void)didChangeValueForKey:(NSString *)key{
    // 通知监听器,某某属性值发生了改变 [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil]; } // 会重写class返回父类的class // 原因:1.为了隐藏这个动态的子类 2.为了让开发者不那么迷惑 - (Class)class{ return [XGPerson class]; } - (void)dealloc{ // 回收工作 } - (BOOL)_isKVOA{ return YES; }

    如何手动调用KVO

    其实通过上面的代码大家已经知道了KVO是怎么触发的了,那怎么手动调用呢?很简单,只要调用两个方法就行了,如下:

        [self.person1 willChangeValueForKey:@"age"];
        [self.person1 didChangeValueForKey:@"age"];

    但是上面说调用顺序的时候,好像明明KVO是在 didChangeVlaueForKey 里面调用的,为什么还要调用 willChangeVlaueForKey呢?

    那是因为KVO调用的时候会去判断这个对象有没有调用 willChangeVlaueForKey 只有调用了这个之后,再调用 didChangeVlaueForKey 才能真正触发KVO

    直接修改成员变量会触发KVO吗?

    答案是不会的,为什么呢?因为KVO是通过修改set方法实现来触发的,一个成员变量都没有 set 方法,所以肯定是不会触发了.

    总结

    KVO是通过runtime机制动态的给要添加KVO监听的对象创建一个子类,并且让instance对象的isa指向这个全新的子类.

    当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数,顺序如下:

    • willChangeValueForKey:
    • 父类原来的setter
    • didChangeValueForKey:

    didChangeValueForKey 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)

    通过这个子类重写一些父类的方法达到触发KVO回调的目的.

    补充

    KVO是使用了典型的发布订阅者设计模式实现事件回调的功能,多个订阅者,一个发布者,简单的实现如下:

    1> 订阅者向发布者进行订阅.

    2> 发布者将订阅者信息保存到一个集合中.

    3> 当触发事件后,发布者就遍历这个集合分别调用之前的订阅者,从而达到1对多的通知.

    以上已全部完毕,如有什么不正确的地方大家可以指出~~ ^_^ 下次再见~~

      

  • 相关阅读:
    javascript页面刷新的几种方法
    Expo大作战(三十九)--expo sdk api之 DocumentPicker,Contacts(获取手机联系人信息),Branch
    Expo大作战(三十八)--expo sdk api之 FileSystem(文件操作系统)
    Expo大作战(三十七)--expo sdk api之 GLView,GestureHandler,Font,Fingerprint,DeviceMotion,Brightness
    Expo大作战(三十六)--expo sdk api之 ImagePicker,ImageManipulator,Camera
    Expo大作战(三十五)--expo sdk api之Location!
    一条SQL语句中算日销售额和月销售额
    绑定sql server数据库的用户与登录名
    牛腩代码生成器
    ASP.NET MVC做的微信WEBAPP中调用微信JSSDK扫一扫
  • 原文地址:https://www.cnblogs.com/xgao/p/9896769.html
Copyright © 2011-2022 走看看