kvo底层窥探

作者: 目前运行时 | 来源:发表于2018-07-04 18:03 被阅读105次

    相信kvo 作为一个ios开发者都用过,但是kvo底层具体怎么实现的,相信很多人也不太明白或者就算有的人明白 但是也说的不是很清楚,我开始也不太明白,自己打算研究一下,下面我来分享一下,如果有错误希望大家给予指正:
    将oc的代码转换成c或者c++代码的命令(比如我转换的main.m文件)
    首先切换到main.m所在的文件位置,然后执行这段命令:

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
    

    如果代码中有__weak
    执行这段命令

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
    
    • kvo 是什么?作用?
      kvo 是键值观察者,key value observer 的缩写,作用是用来观察对象的属性改变的。
    • kvo的简单使用,下面我沾上我的代码,里面不经常用的参数,我做了一一的说明
    
    #import "ViewController.h"
    #import "DGPerson.h"
    #import <objc/runtime.h>
    
    @interface ViewController ()
    
    @property (strong, nonatomic) DGPerson *person1;
    //@property (strong, nonatomic) DGPerson *person2;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 动态生成这个类isa ---- NSKVONotifying_DGPerson
        self.person1 = [[DGPerson alloc] init];
        // 因为没有添加观察者 所以不生成中间类 而是isa --- 指向 DGPerson
        self.person1.age = 10;
        
    //    self.person2 = [[DGPerson alloc] init];
    //    self.person2.age = 20;
        // 查看他们调用那一个方法
    //    NSLog(@"调用之前的 调用方法的地址 :%p %p",[self.person1 methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
        
        // 添加观察者。观察者的主要目的是 观察对象的属性的改变
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
     [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"sasd"];
    #pragma mark - 添加了观察者的之后的方法调用foundation的这个方法_NSSetIntValueAndNotify
        //  P (IMP) 0x10daf2790 :通过地址打印调用的方法
        
    //   NSLog(@"调用之后的 调用方法的地址 :%p %p",[self.person1 methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
        NSLog(@"asdahsdajsdajsd");
    
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        self.person1.age = 20;
    //    self.person2.age = 30;
    
    }
    /**
     观察者的回去调用的代理
    
     @param keyPath 那个属性
     @param object 那个对象
     @param change 改变的字典
     @param context 传递的数据
     */
    -(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"];
    }
    @end
    

    解释:其中注释的地方 大家不要看 ,先看没有注释的地方,可以看到我们改变age的值开始调用了observeValueForKeyPath。。。。。的这个方法,其中observeValueForKeyPath这个方法的参数我在注释的时候都做了说明,特别注意的是context:苹果的官方文档的解释是:注册观察者以接收键值观察通知时提供的值,其实我们可以简单的理解为传递的参数数据。
    以上的打印信息为:

    2018-07-04 16:52:42.828077+0800 kvo的底层原理窥探[2581:264281] <DGPerson: 0x600000018440> --- 的 age 属性值发生了改变,变化为 {
        kind = 1;
        new = 20;
        old = 10;
    } 
    
    • kvo本质使用不是我们今天的重点,我们的重点是研究kvo 到底是怎么一回事.
      1.重新创建一个person对象,命名为person2 ,其中person2不添加观察者, 然后我们看看他们的isa指针指向的是什么?


      image.png

      我们大胆的猜测 也就是添加了person1 添加了观察者,添加了观察者的对象他会在运行的时候时候动态生成一个中间类NSKVONotifying_DGPerson,怎么生成的,因为我们的oc 是一门动态的语言,对象添加了观察者,在运行时候 runtime会动态给他生成一个中间类。
      2.我们在我们的app中创建一个类 名字就叫NSKVONotifying_DGPerson,运行程序 发现观察者失败了:


      image.png
      这也间接证明了,确实他在运行的时候创建了一个NSKVONotifying_DGPerson,其中我的person对象是DGPerson,你自己的项目他创建的肯定和你的类名字一至的,我们知道了 他创建一个类 他创建了什么方法,怎么执行的呢?
      3.执行顺序,其中我们在添加观察者前后来看下他的调用方法的内存地址,通过方法的内存地址我们来在lldb中输入命令 来看他执行的方法(p (IMP) 内存地址),通过输入命令 我们可以看到
      image.png

      他调用的是这个方法:_NSSetIntValueAndNotify 这个方法一看就是c语言的方法,那我们可以大致猜一下,在这个类中NSKVONotifying_DGPerson 他的底层怎么实现的

    #import "NSKVONotifying_DGPerson.h"
    
    @implementation NSKVONotifying_DGPerson
    
    - (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];
    }
    

    解释:其中 以上的代码是我估计的,其实苹果是闭元的,我们看不到具体怎么实现,但是如果你的手机是越狱的 可以通过ifunbox这个软件来找到fundation框架中的这个方法,确实有的 我找过 因为现在的我手机不是越狱的 我没法演示了,通过反汇编的软件来看到他的汇编的代码,那个软件可以生成伪代码,她的伪代码和我写的基本差不多。

    • 下面我们来看一个现象,我写了一句这样的代码
       Class person1Class = object_getClass(self.person1);
        Class person1Cls = [self.person1 class];
         NSLog(@"%@ --- %@",person1Class,person1Cls);
        Class person2Class = object_getClass(self.person2);
        Class person2Cls = [self.person2 class];
         NSLog(@"%@ --- %@",person2Class,person2Cls);
    

    发生现象:


    image.png

    解释:要明白这个,需要知道这个函数object_getClass的作用,她的作用是:
    1.传递一个实例对象生成的是类对象
    2.传递一个类对象生成的是原类对象
    3.传递原类对象生成的是基类的原类对象
    以上的打印信息,可以证明添加了观察这的对象他在这个NSKVONotifying_DGPerson类中重写了class方法,为什么呢 因为如果不重写 那么我们的调用[self.person1 class]这个方法 应该返回的是:NSKVONotifying_DGPerson而不是DGPerson,我猜测苹果这样写 就是为了让我们普通开发者不知道NSKVONotifying_DGPerson,为什么object_getClass 就能返回他本身的类呢?因为object_getClass 是runtime的方法,他拿到的就是我们真实的方法。怎么证明NSKVONotifying_DGPerson确实重写了class方法呢?我用下面的代码进行证实。

    - (void)methodWithClass:(Class)cls {
        
        unsigned int count;
        Method *methodList = class_copyMethodList(cls, &count);
        NSMutableString *nameStr = [NSMutableString string];
        
        for (NSInteger index = 0; index < count; index ++) {
            Method method = methodList[index];
            NSString *methodName = NSStringFromSelector(method_getName(method));
            [nameStr appendFormat:@" + %@",methodName];
        }
        NSLog(@"nameStr --- %@",nameStr);
        // c语言的函数要释放
        free(methodList);
    }
    

    打印:


    image.png

    可以看到 其实他是实现了四个方法 其中有class,我们大致猜测一下 ,class的实现应该是这样的

    - (Class)class{
    return [DGPerson class];
    }
    
    • 怎样手动触发kvo?
      大致思路是:我们首先禁止kvo自动发送通知的方法,二是我们需要手动调用willChangeValuesForKey:(NSString *)key;和didChangeValuesForKey::(NSString *)key的方法;代码如下
    #import "DGPerson.h"
    
    @implementation DGPerson
    
    
    -(void)setAge:(int)age {
        if (age < 0) {
            _age = age;
        }else{
            [self willChangeValueForKey:@"age"];
            _age = age;
            [self didChangeValueForKey:@"age"];
        }
    }
    /**
     是否自动发送通知呢
    
     @return no:不自动发送通知
     */
    +(BOOL)automaticallyNotifiesObserversOfAge{
        return NO;
    }
    @end
    

    注意:需要添加观察者 ,不添加观察者无效

    • kvo删除尽量不要用keypath这种方式进行删除,因为如果我是DGStudent继承的DGPerson 我在DGPerson添加了观察者,我在DGStudent也添加了观察者,那么我dealloc 都进行了删除,那么删除的同一个keypath的时候就会crash。
    • 其中DGStudent 继承 DGPerson,在DGStudent添加了观察者,在DGPerson中也添加了观察者,那么只会调用 DGStudent中的观察者,其中在oneViewController 中添加了观察者,也在viewController中添加了观察者,那么在oneViewController中进行调用,要想调用父类中的观察者回调,需要这样书写:
    #import "DGTwoViewController.h"
    #import "DGStudent.h"
    
    @interface DGTwoViewController ()
    
    @end
    
    @implementation DGTwoViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        
        [self.student addObserver:self forKeyPath:@"weight" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"weight"];
        [self.student addObserver:self forKeyPath:@"height" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"height"];
    }
    /**
     观察者的回去调用的代理
    
     @param keyPath 那个属性
     @param object 那个对象
     @param change 改变的字典
     @param context 传递的数据
     */
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"------------------");
        NSLog(@"%@ --- 的 %@ 属性值发生了改变,变化为 %@  传递给观察者的参数 %@",object,keyPath,change,context);
        if (([object isKindOfClass:[DGStudent class]]) && ([keyPath isEqualToString:@"weight"] || [keyPath isEqualToString:@"height"])) {
            NSLog(@"我们是好孩子");
        }else{
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    
    }
    #pragma mark - 其他的相关的处理
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        self.student.weight = 50;
        self.student.height = 50;
    }
    
    -(void)dealloc {
        [self.student removeObserver:self forKeyPath:@"age"];
    }
    @end
    头文件:
    #import "DGViewController.h"
    @interface DGTwoViewController : DGViewController
    @end
    

    相关文章

      网友评论

        本文标题:kvo底层窥探

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