美文网首页iOS
iOS底层探索之KVO(二)—KVO原理分析

iOS底层探索之KVO(二)—KVO原理分析

作者: 俊而不逊 | 来源:发表于2021-08-05 14:18 被阅读0次

    回顾

    上一篇博客中,已经介绍了KVO的相关操作,那么接下来就去探索一下KVO的底层逻辑,KVO到底是如何实现的呢?

    文章主题
    • 在官方文档中有如下图中的说明


      isa-swizzling

    键值观察是使用称为isa-swizzling的技术实现的。

    • isa指针,顾名思义,指向对象的类,它保持一个调度表。该调度表主要包含指向类实现的方法的指针,以及其他数据。

    • 当观察者为对象的属性注册时,被观察对象的 isa指针被修改,指向中间类不是真正的类。因此,isa指针的值不一定反映实例的实际类。

    • 你不应该依赖isa指针来确定类的成员。相反,应该使用该class方法来确定实例对象的类。

    1. isa-swizzling验证

    在添加观察者处,打上断点,再控制台lldb调试看看。

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.student = [[JPStudent alloc]init];
        [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    }
    
    • 控制台打印如下


      lldb调试

    addObserverstudentJPStudent变成了NSKVONotifying_JPStudent

    我们知道,实例对象的关系实际上就是实例对象的isa指向了类对象。所以这里我们可以推断,self.student在调用addObserver方法后,已经从JPStudent类的实例对象,变成了NSKVONotifying_JPStudent
    的实例对象。

    2. NSKVONotifying_JPStudent子类验证

    • 那么这个NSKVONotifying_JPStudent是什么东西呢?是一开始就直接存在,还是和JPStudent类之间有什么关系呢?那么看看NSKVONotifying_JPStudent是不是一开始就存在的,再次运行代码,断点还是断在添加观察者者处,打印一下
      NSKVONotifying_JPStudent生产测试

    提醒objc_getClassruntimeapi,一定要导入头文件才可以正常使用,如图所示。

    在调用addObserver方法前后分别打印,结果说明NSKVONotifying_JPStudent是系统动态生成添加的一个类。这两个类名字这么相似,有没有可能是JPStudent的子类呢?我们打印一下看看

    验证NSKVONotifying_JPStudent是JPStudent的子类

    从打印来看,发现了新大陆,NSKVONotifying_JPStudent 确实是继承自JPStudent的。那么这个中间类,有没有可能存在自己的子类呢?我们通过下面这段代码来看看

    #pragma mark - 遍历类以及子类
    - (void)printClasses:(Class)cls{
         // 注册类的总数
         int count = objc_getClassList(NULL, 0);
         // 创建一个数组, 其中包含给定对象
         NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
         // 获取所有已注册的类
         Class* classes = (Class*)malloc(sizeof(Class)*count);
         objc_getClassList(classes, count);
         for (int i = 0; i<count; i++) {
              if (cls == class_getSuperclass(classes[i])) {
                    [mArray addObject:classes[i]];
              }
         }
         free(classes);
         NSLog(@"classes = %@", mArray);
    }
    
    • 打印结果如下
      遍历类以及子类
      从打印来看,可以验证NSKVONotifying_JPStudentJPStudent的子类。
      那么NSKVONotifying_JPStudent这个类里面都有些什么内容呢?类里面一般也就是存储了成员变量方法协议等信息,那么通过下面这段代码来看看它里面都有什么。
    #pragma mark **- 遍历方法-ivar-property**
    - (void)printClassAllMethod:(Class)cls{
        unsigned int count = 0;
        Method *methodList = class_copyMethodList(cls, &count);
        for (int i = 0; i<count; i++) {
            Method method = methodList[i];
            SEL sel = method_getName(method);
            IMP imp = class_getMethodImplementation(cls, sel);
            NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
        }
        free(methodList);
    }
    
    • 打印如下


      遍历方法-ivar-property

    从打印结果来看,系统重写了setNickNameclassdealloc这几个方法,并且添加了一个叫_isKVOA的方法,来区分是不是系统通过KVO自动生成的。

    3. 观察者被移除isa的指向?

    官方文档中说:调用addObserver方法会修改isa指向,那么现在我们移除观察者后系统会怎么做呢?我们在dealloc方法中移除观察者这里打上断点,然后继续观察self.studentisa指向。

    观察者被移除isa的指向?

    移除观察者之后,self.studentisa又指回了JPStudent类。并且生成的子类NSKVONotifying_JPStudent 还在,没有进行销毁。

    原因是如果下次继续进行观察者添加,系统就不会再生成新的中间类,而是直接使用这个类了,防止资源的浪费。

    4. class方法

    在添加观察者之后,我们都知道会生成动态子类NSKVONotifying_JPStudent

    那么调用class方法p self.student.class打印的是 NSKVONotifying_JPStudent 吗???

    • 断点在添加观察者之后,我们控制台验证一下
      self.student.class
      从打印结果看,输出的还是JPStudent,虽然self.studentisa已经指向NSKVONotifying_JPStudent了,但是由于NSKVONotifying_JPStudent重写了class方法,最后打印输出的还是JPStudent ,苹果这么做的目的是为了隐藏系统在背后做的一系列操作,让开发者更少的关注底层逻辑,只关注上层的代码实现就可以。

    5. setter方法

    既然重写了setter方法观察属性,那么如果有成员变量,是否也可以能观察呢?增加age成员变量,测试一下

    @interface JPStudent : NSObject
    {
        @public
        int age;
    }
    @property (nonatomic, copy) NSString *name;
    
    @end 
    
    setter方法测试

    当对age进行赋值的时候,并没有触发监听的回调方法。那么就说明了只是对属性的setter方法进行的监听。

    我们再看看在dealloc中观察者移除isa指回的时候,查看name的值

    观察者移除后Name的值

    那就说明在KVO生成的类中对name的修改影响到了原始类。

    name下个内存断点调试看看

    断点调试
    • bt 打印堆栈信息


      bt 打印堆栈信息

    发现调用了Foundation的一些方法,最后才是[JPStudent setName:]name赋值。

    提示Foundation框架是不开源的。

    • Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
    • Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
    • Foundation`_NSSetObjectValueAndNotify

    _NSSetObjectValueAndNotify汇编调用主要如下:

    _NSSetObjectValueAndNotify
    "willChangeValueForKey:"
    这里是调用setter方法赋值
    "didChangeValueForKey:"
    "_changeValueForKey:key:key:usingBlock:"
    

    从堆栈信息和汇编可以知道,在_changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:中进行赋值后的回调,那么肯定得通知监听者

    • observeValueForKeyPath的回调中打个断点:
    在这里插入图片描述

    确认是在NSKeyValueNotifyObserver通知中进行的回调。

    6.总结

    • KVO添加观察者addObserver动态生成子类NSKVONotifying_XXX
    • 重写class方法,返回父类class信息。父类isa指向子类。
      给动态子类添加setter方法(所有要观察的属性)。
      消息转发给父类。
    • setter会调用父类原来的方法进行赋值,完成后进行回调通知。
    • 移除observer的时候isa指回父类,动态生成的子类并不会销毁.

    更多内容持续更新

    🌹 喜欢就点个赞吧👍🌹

    🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

    🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

    相关文章

      网友评论

        本文标题:iOS底层探索之KVO(二)—KVO原理分析

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