iOS窥探KVO底层实现原理篇

640?wx_fmt=gif

640?wx_fmt=jpeg

Linux编程 点击右侧关注,免费入门到精通! 640?wx_fmt=jpeg


作者丨大兵布莱恩特
https://www.jianshu.com/p/0aa83ac521ba


最近小编公司招聘 iOS, 于是小编从网上找了几道面试题,来考察候选人iOS 开发方面的技术水平,其中有一道面试题便是


KVO 底层实现是什么?


如何手动出发 KVO?


修改成员变量的值会出发 KVO 吗?


KVC 赋值会出发 KVO 吗?


当你了解 KVO 实现原理后,这几道面试题自然不在话下.接下来我将通过代码和讲解来窥探 KVO 背后的奥秘.


640?wx_fmt=other


首先创建一个 Person 类 内部有个 name 属性,然后 创建p1 和 p2两个实例对象,其中p1添加了kvo监听,p2没有添加 kvo 监听,然后重写了 observeValueForKeyPath 方法 监听Person.name 属性发生改变时候的通知.


640?wx_fmt=other


从本质上来看 Person 给name赋值的时候 调用的是 setName 方法 ,无论 p1还是p2 调用的 setter 方法都是一样的,为什么 p1改变 name 属性值就能有通知, p2确没有,调用的 都是同一个 setName:(NSString *)name 方法,区别怎么那么大?


640?wx_fmt=gif小编窥探尝试1


接下来小编打印下p1和p2的内存地址 看看p1和p2内存地址能不能一探究竟.


640?wx_fmt=other


从 p1和 p2内存地址上也看不出来什么东东.


640?wx_fmt=gif小编窥探尝试2 打印 p1和 p2 的 class 信息


640?wx_fmt=other


what 什么 输出的 class 都是 Person 类 ,既然同一个类 同一个 setter 方法,为什么我们不一样呢?


小编窥探尝试3 打印 object_getClass 试试看


我们都知道object_getClass(id) 才会返回这个实例对象的真实 class 类型


640?wx_fmt=other


什么 , 添加 KVO 之后说好的 Person 类跑哪去了, NSKVONotifying_Person是什么东东?


为了进一步窥探 KVO 添加前后的变化


小编窥探尝试4 打印 setName 方法实现IMP指针有没有发生改变,我们知道同一个方法的实现 IMP 地址是不变的.


640?wx_fmt=other


连 setName方法都不一样了 , 为了一探究竟 小编绝对对上边的 NSKVONotifying_Person 和 添加 KVO 之后的 imp 指针进行进一步研究.


首先 在 lldb 上输入 imp1和 imp2


640?wx_fmt=other


发生了 imp1 方法实现在 Foundation 框架里的 _NSSetObjectValueAndNotify 函数中 ,而 imp2 则调用了 Person setName 方法


640?wx_fmt=other


也就是说添加了 KVO 之后 p1 修改 name 值之后 不再调用 Person 的 setName方法 ,而 p2没有添加 kvo 监听 依然正常调用 setName:方法 ,由此可以得出 p1 添加完 KVO 监听后 系统修改了默认方法实现,那么既然没有调用 setName: 方法 为什么 p1.name 的值也发生了改变?


接下来我们准备对刚才 NSKVONotifying_Person 类进行下一步研究, NSKVONotifying_Person 和 Person 有没有内在的联系呢?


小编窥探尝试5 NSKVONotifying_Person和 Person 之间的联系时什么


640?wx_fmt=other


通过打印 NSKVONotifying_Person 的 superclass  和 Person 的 superclass 可以得出, NSKVONotifying_Person是一个 Person 子类,那么为什么苹果会动态创建这么一个 子类呢? NSKVONotifying_Person 这个子类 跟 Person 内部有哪些不同呢 ?


这个时候 我们去输出下 Person 和 NSKVONotifying_Person 内部的方法列表 和 属性列表 ,看看NSKVONotifying_Person 子类都添加了那些方法和属性.


- (void)viewDidLoad {
    [super viewDidLoad];


    Person *p1 = [[Person alloc] init];
    Person *p2 = [[Person alloc] init];

    id cls1 = object_getClass(p1);
    id cls2 = object_getClass(p2);
    NSLog(@"添加 KVO 之前: cls1 = %@  cls2 = %@ ",cls1,cls2);

    [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
     cls1 = object_getClass(p1);
     cls2 = object_getClass(p2);


    NSString *methodList1 = [self printPersonMethods:cls1];
    NSString *methodList2 = [self printPersonMethods:cls2];

    NSLog(@"%@",methodList1);
    NSLog(@"%@",methodList2);


//  NSLog(@"添加 KVO 之后: cls1 = %@  cls2 = %@ ",cls1,cls2);

//  id super_cls1 = class_getSuperclass(cls1);
//  id super_cls2 = class_getSuperclass(cls2);
//
//  NSLog(@"super_cls1 = %@ ,super_cls2 = %@",super_cls1,super_cls2);
//
//  p1.name = @"dzb";
//  p2.name = @"123";

}

- (NSString *) printPersonMethods:(id)obj {

    unsigned int count = 0;
    Method *methods = class_copyMethodList([obj class],&count);
    NSMutableString *methodList = [NSMutableString string];
    [methodList appendString:@"[ "];
    for (int i = 0; i<count; i++) {
        Method method = methods[i];
        SEL sel = method_getName(method);
        [methodList appendFormat:@"%@",NSStringFromSelector(sel)];
        [methodList appendString:@" "];
    }

    [methodList appendFormat:@"]"];

    free(methods);

    return methodList;
}


640?wx_fmt=other


从输出结果可以看出来  NSKVONotifying_Person 内部也有一个 setName:方法  还重写了 class 和 dealloc 方法 , _isKVOA, 那么我们可以大致的得出, p1添加 kVO 后 runtime 动态的生成了一个  NSKVONotifying_Person子类 并重写了 setName 方法 ,那么 setName 内部一定是做了一些事情,才会触发  observeValueForKeyPath 监听方法.


继续探究 NSKVONotifying_Person 子类 重写 setName 都做了什么?


其实 setName 方法内部 是调用了 Foundation 的 _NSSetObjectValueAndNotify 函数 ,在 _NSSetObjectValueAndNotify 内部


1首先会调用 willChangeValueForKey


2然后给 name 属性赋值


3 最后调用 didChangeValueForKey


4最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .


640?wx_fmt=other


由于苹果 Foundation 框架是不开源的 ,所以我们依然可以通过重写Person 的  willChangeValueForKey  和  didChangeValueForKey 验证我们的猜想 .


640?wx_fmt=other


首先当我们改变p1.name 的值时 并不是首先执行的 setName: 这个方法 ,而是先调用了  willChangeValueForKey  其次 调用父类的 setter 方法 对属性赋值 ,然后再调用  didChangeValueForKey 方法 ,并在 didChangeValueForKey 内部 调用监听器的  observeValueForKeyPath方法 告诉外界 属性值发生了改变.

640?wx_fmt=other


640?wx_fmt=other


至于重写了 dealloc 和 class 方法 是为了做一些 KVO 释放内存 和 隐藏外界对于 NSKVONotifying_Person 子类的存在


640?wx_fmt=other


这就是我们调用 [p1 class] 和 [p2 class]结果都显示 Person 类 ,让我们误以为 Person 没有发生变化


补充说明 ,KVC 对属性赋值时候 是会在这个类里边 去查找 _age  isAge setAge setIsAge 等方法的 ,最终会调用属性的 setter 方法 ,那么如果添加了 KVO 还是会被触发的 .


相反 设置成员变量  _age 由于不会触发 setter 方法 ,因此不会去触发 KVO 相关的代码 .


640?wx_fmt=other


 推荐↓↓↓ 

640?wx_fmt=png

👉16个技术公众号】都在这里!

涵盖:程序员大咖、源码共读、程序员共读、数据结构与算法、黑客技术和网络安全、大数据科技、编程前端、Java、Python、Web编程开发、Android、iOS开发、Linux、数据库研发、幽默程序员等。

640?wx_fmt=png万水千山总是情,点个 “ 好看” 行不行

更多精彩内容