美文网首页
03 iOS底层原理 - KVO本质探究

03 iOS底层原理 - KVO本质探究

作者: 程序小胖 | 来源:发表于2020-01-29 22:16 被阅读0次

    废话不多说先来几个面试题:

    一,iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
    二,如何手动触发KVO
    三,直接修改成员变量会触发KVO吗?

    通过挖掘KVO的本质,就会发现,这几个面试题就跟切菜一样

    那么什么是KVO呢?

    什么是KVO呢?
    KVO的全称是Key-Value-Observing,即“键值监听”,可以用于监听某个对象属性值的变化

    用一张简单的图就可以表示:

    image.png

    这就是KVO最简单的是使用,那么它到底是怎么实现的呢,下面我们一步步解开这个KVO底层的神秘面纱。

    一,代码准备

    1. 创建一个Person 类 继承NSObject
    // 声明
    @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
    
    2. ViewController代码
    // 导入头文件
    #import <objc/runtime.h>
    #import "Person.h"
    
    // 声明person对象
    @interface ViewController ()
    @property (nonatomic, strong) Person *person1;
    @property (nonatomic, strong) Person *person2;
    @end
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        person1 = [[Person alloc]init];
        person1 = 18;
    
        self.person2 = [[Person alloc]init];
        self.person2.age = 28;
        // 给person1添加一个KVO
        [person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];
    }
    
    // 当监听对象的属性值发生改变是,就会调用
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", 
    object, keyPath, change, context);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.person1.age = 20;
    }
    

    二,开始研究KVO本质

    点击屏幕后,会触发observeValueForKeyPath这监听事件
    // 打印结果

    监听到<Person: 0x600002e2c510>的age属性值改变了 - {
             kind = 1;
             new = 20;
             old = 18;
     }
    

    发现age的值确实发生了变化

    1. 那么age值的改变是否与setAge:方法有关

    在touchesBegan给person2重新赋值

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //    self.person1.age = 21;
    //    self.person2.age = 22;
    //  上面两句代码,其实就是调用Person的 set 方法,都在调用同一个方法
        [self.person1 setAge:21];
        [self.person2 setAge:22];
    }
    

    当触发touchesBegan事件时,发现调用 [self.person1 setAge:21];后会触发observe监听器,而调用 [self.person2 setAge:22];后不会触发observe监听器。
    既然都是在调用 setAge方法,但是为啥只有person1的age发生改变时,才会给observe发通知呢?而person2的age发生改变时,不会通知observe?
    先看两张图:

    image.png image.png
    从上面两张图,可以看出来:     
    其实本质上就是两个实例对象的isa指向不一样
    现在看下两个实例的isa有什么不同
    lldb结果:
    self.person1.isa -> NSKVONotifying_Person
    self.person2.isa -> Person
    差异:
    1. 添加了KVO监听的实例person1,通过isa指向的class是NSKVONotifying_Person
    2. 没有添加KVO监听的实例person2,通过isa指向的class是Person
    那么,NSKVONotifying_Person这个类是怎么产生的呢?
    本质上是利用Runtime动态创建的一个类(可以利用truntime机制自己实现KVO监听)
    

    结论:由此可见,age值的改变与setAge:方法没有关系,而是因为派生出了新的类,此时的setAge:方法的实现也就不一样了,具体有啥不易样的,接着往下看哈。

    2. person1添加KVO前后person1和person2对象的变化

    验证person1在添加监听前后,person1实例的isa指向的【class】和具体的【对象方法】实现到底发生了哪些变化

    1. 利用runtime的 object_getClass() 查看person1添加监听前后的,person1和person2的isa指向类对象;
    2. 利用runtime的 object_getClass() 查看person1添加监听前后的,person1和person2的isa指向类对象的地址;
    3. 利用runtime的 methodForSelector这个方法来获取setAge:这个实例方法的具体实现
      根据以上3点,添加一些打印信息:
        NSLog(@"person1添加监听之前类对象:%@, %@",
              object_getClass(self.person1),
              object_getClass(self.person2));
        NSLog(@"person1添加监听之前类对象地址:%p, %p",
              object_getClass(self.person1),
              object_getClass(self.person2));
        NSLog(@"person1添加监听之前类的实例方法实现:%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添加监听之后类对象:%@, %@",
              object_getClass(self.person1),
              object_getClass(self.person2));
        NSLog(@"person1添加监听之后类对象地址:%p, %p",
              object_getClass(self.person1),
              object_getClass(self.person2));
        NSLog(@"person1添加监听之后类的实例方法实现:%p, %p",
              [self.person1 methodForSelector:@selector(setAge:)],
              [self.person2 methodForSelector:@selector(setAge:)]);
    

    分析打印信息:
    打印结果:

    person1添加监听之前类对象:Person, Person
    person1添加监听之后类对象:NSKVONotifying_Person, Person
    
    person1添加监听之前类对象地址:0x1089bd708, 0x1089bd708
    person1添加监听之后类对象地址:0x6000021d87e0, 0x1089bd708
    

    结论:
    由此可见,person1在添加监听后,isa所指向的类确实改变了,那么setAge:这个对象方法到底是怎么实现的呢,继续分析第三个打印结果

    打印结果:

    person1添加监听之前类的实例方法实现:0x1061e1ed0, 0x1061e1ed0
    person1添加监听之后类的实例方法实现:0x7fff257223da, 0x1061e1ed0
    
    利用lldb查看
    (lldb) p (IMP)0x1061e1ed0
    (IMP) $0 = 0x00000001061e1ed0 (06-KVO初探`-[Person setAge:] at Person.m:13)
    (lldb) p (IMP)0x7fff257223da
    (IMP) $1 = 0x00007fff257223da (Foundation`_NSSetIntValueAndNotify)
    

    结论:
    由此可见,person1添加监听后,setAge:的实现的确改变了,是在Foundation框架下的_NSSetIntValueAndNotify(c语言方法)实现的,_NSSetIntValueAndNotify具体实现见自己实现的NSKVONotifying_Person 类。

    下面可以看下_NSSetIntValueAndNotify实现的伪代码

    // 声明一个添加了KVO的派生类 NSKVONotifying_Person
    @interface NSKVONotifying_Person : Person
    @end
    
    // 实现
    @implementation NSKVONotifying_Person
    - (void)setAge:(int)age {
        _NSSetIntValueAndNotify();
    }
    // 伪代码 大概流程
    void _NSSetIntValueAndNotify() {
        [self willChangeValueForKey:@"age"];
        [super setAge:age]; 
        [self didChangeValueForKey:@"age"];
    }
    - (void)didChangeValueForKey:(NSString *)key {
        // 伪代码
        // 通知监听器,某某属性发生了改变
        [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
    }
    @end
    
    3. 查看person1添加KVO后,都有哪些调用

    在Person类里,实现两个父类的方法,并且给setAge加上打印信息

    - (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");
    }
    

    触发touchesBegan方法

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self.person1 setAge:21];
    //    [self.person2 setAge:22];
    }
    

    打印结果是:

    哥们 == willChangeValueForKey
    哥们 == setAge
    哥们 == didChangeValueForKey-begin
    监听到<Person: 0x600001108590>的age属性值改变了 - {
        kind = 1;
        new = 21;
        old = 1;
    } - 123
    哥们 == didChangeValueForKey-end
    

    打印结果分析:添加了KVO的对象,属性改变的话,都有下面一些方法调用

    当修改instance对象的属性时,先调用setter方法,然后实现Foundation中的_NSSet****ValueAndotify函数
    a> willChangeValueForKey:
    b> 父类原来的setter
    c> didChangeValueForKey:
    内部会触发监听器(Observe)的监听方法(observeValueForKeyPath:ofObject:change:context:)

    如果person2调用了setAge:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        // [self.person1 setAge:21];
        [self.person2 setAge:22];
    }
    

    打印结果只有一个

    哥们 == setAge
    

    说明:没有添加监听的对象,willChangeValueForKey和didChangeValueForKey方法不会调用

    4. 查看NSKVONotifying_Person类的元类

    验证一下,添加KVO后,新生成的NSKVONotifying_Person类对象的isa指向的元类对象及对象地址是什么

    利用runtime的 Class object_getClass(id obj) 来查看对象地址和对象名
    1> 传入的obj可能是instance对象、class对象、meta-class对象
    2> 返回值
    a) 如果是instance对象,返回class对象
    b) 如果是class对象,返回meta-class对象
    c) 如果是meta-class对象,返回NSObject(基类)的meta-class对象

    为了验证在添加KVO后添加一些打印信息

    NSLog(@"类对象地址:%p, %p",
    // 拿到person1.isa,相当于Person类地址(isa & ISA_MASK)
              object_getClass(self.person1), 
    // 拿到person2.isa
              object_getClass(self.person2)); 
    
    NSLog(@"元类对象地址:%p, %p",
    // 拿到person1.isa.isa
              object_getClass(object_getClass(self.person1)), 
    // 拿到person2.isa.isa
              object_getClass(object_getClass(self.person2))); 
        
    NSLog(@"类对象:%@, %@",
    // 拿到person1的isa指向的类
              object_getClass(self.person1), 
    // 拿到person2的isa指向的类
              object_getClass(self.person2)); 
    
    NSLog(@"元类对象:%@, %@",
     // 拿到person1的isa指向的类的元类
              object_getClass(object_getClass(self.person1)),
     // 拿到person2的isa指向的类的元类
              object_getClass(object_getClass(self.person2)));
    

    打印信息分析:

    类对象地址:0x600001284120, 0x1081ea710
    元类对象地址:0x6000012841b0, 0x1081ea6e8
    
    类对象:NSKVONotifying_Person, Person
    元类对象:NSKVONotifying_Person, Person
    

    结论:
    从打印结果来看,派生出来的NSKVONotifying_Person类的元类就是它本身

    三, 派生出来的NSKVONotifying_Person都有哪些方法

    先看一张示例图,红框狂起来的部分

    image.png
    1. 利用runtime来获取方法实现
    Class cls = object_getClass(self.person1);
        unsigned int count;
        Method *methodList = class_copyMethodList(cls, &count);
        // 遍历方法
        for (int i = 0; i < count; i++) {
            // 获得方法
            Method method = methodList[i];
            NSString *methodName = NSStringFromSelector(method_getName(method));
            NSLog(@"methodName == %@", methodName);
        }
    

    打印结果:

    setAge:
    class
    dealloc
    _isKVOA
    

    所以派生出来的NSKVONotifying_Person类里面,确实存在setAge:/class/dealloc/_isKVOA这些方法

    2. 研究下为啥要重写class方法呢?

    添加两个打印信息

    NSLog(@"类对象:%@, %@", object_getClass(self.person1), object_getClass(self.person2));
    NSLog(@"类对象:%@, %@", [self.person1 class], [self.person2 class]);
    

    上面打印信息是利用两种方法来获取person1和person2的类对象
    打印结果:

    类对象:NSKVONotifying_Person, Person
    类对象:Person, Person
    

    结果分析:
    只有通过runtime的object_getClass这中方式,取出来的才是person1的isa指向的类对象,
    而通过self.person1 class]取出来的,不一定是person1的isa指向的类对象

    为什么要重写class呢?
    猜测:系统为了直接返回当前类,屏蔽内部实现,

    如果不重写,person1找到isa,通过isa找到对应的类对象,在这个类里面发现没有-class的这个方法,那么就通过superclass找到父类,
    如果父类还没有,就通过superclass一直找到基类,基类(NSObject)里面就会通过object_getClass(sef)返回当前的类,即Person

    四,回答面试题

    一,iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
     1. 利用runtimeAPI动态生成一个子类,并且让instance的isa指向一个全新的子类
     2. 当修改instance对象的属性时,现调用setter方法,然后实现Foundation中的_NSSet****ValueAndotify函数
     a> willChangeValueForKey:
     b> 父类原来的setter
     c> didChangeValueForKey:
        内部会触发监听器(Observe)的监听方法(observeValueForKeyPath:ofObject:change:context:)
     
     二,如何手动触发KVO
     意思就是不通过改变属性的值,怎么触发KVO
     手动调用willChangeValueForKey:和didChangeValueForKey:
     
     三,直接修改成员变量会触发KVO吗?
     不会触发KVO,因为只有通过setter方法才能触发,而成员变量不会调用setter方法
     要想通过修改成员变量来触发KVO,也很简单
     手动依次调用:
     1.willChangeValueForKey:
     2.修改成员变量
     3.didChangeValueForKey:
    

    相关文章

      网友评论

          本文标题:03 iOS底层原理 - KVO本质探究

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