iOS窥探KVO底层原理

最近小编公司招聘 iOS, 于是小编从网上找了几道面试题,来考察候选人iOS 开发方面的技术水平,其中有一道面试题便是
KVO 底层实现是什么?
如何手动出发 KVO?
修改成员变量的值会出发 KVO 吗?
KVC 赋值会出发 KVO 吗?
当你了解 KVO 实现原理后,这几道面试题自然不在话下.接下来我将通过代码和讲解来窥探 KVO 背后的奥秘.

image.png

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

image.png

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

小编窥探尝试1

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

image.png

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

小编窥探尝试2 打印 p1和 p2 的 class 信息

image.png

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

小编窥探尝试3 打印 object_getClass 试试看
我们都知道object_getClass(id) 才会返回这个实例对象的真实 class 类型

image.png

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

为了进一步窥探 KVO 添加前后的变化
小编窥探尝试4 打印 setName 方法实现IMP指针有没有发生改变,我们知道同一个方法的实现 IMP 地址是不变的.

image.png

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

首先 在 lldb 上输入 imp1和 imp2

image.png

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

image.png

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

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

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

image.png

通过打印 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:@"[\n"];
    for (int i = 0; i<count; i++) {
        Method method = methods[i];
        SEL sel = method_getName(method);
        [methodList appendFormat:@"%@",NSStringFromSelector(sel)];
        [methodList appendString:@"\n"];
    }

    [methodList appendFormat:@"]"];

    free(methods);

    return methodList;
}

image.png

从输出结果可以看出来 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 去告诉监听器属性值发生了改变 .

image.png

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

image.png

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

Untitled.gif

image.png

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

image.png

这就是我们调用 [p1 class] 和 [p2 class]结果都显示 Person 类 ,让我们误以为 Person 没有发生变化
补充说明 ,KVC 对属性赋值时候 是会在这个类里边 去查找 _age isAge setAge setIsAge 等方法的 ,最终会调用属性的 setter 方法 ,那么如果添加了 KVO 还是会被触发的 .
相反 设置成员变量 _age 由于不会触发 setter 方法 ,因此不会去触发 KVO 相关的代码 .

image.png

好了,我是大兵布莱恩特,欢迎加入博主技术交流群,iOS 开发交流群

QQ20180712-0.png

猜你喜欢

转载自blog.csdn.net/dzb1060545231/article/details/80953110