最近小编公司招聘 iOS, 于是小编从网上找了几道面试题,来考察候选人iOS 开发方面的技术水平,其中有一道面试题便是 KVO 底层实现是什么? 如何手动出发 KVO? 修改成员变量的值会出发 KVO 吗? KVC 赋值会出发 KVO 吗? 当你了解 KVO 实现原理后,这几道面试题自然不在话下.接下来我将通过代码和讲解来窥探 KVO 背后的奥秘.
首先创建一个 Person 类 内部有个 name 属性,然后 创建p1 和 p2两个实例对象,其中p1添加了kvo监听,p2没有添加 kvo 监听,然后重写了 observeValueForKeyPath 方法 监听Person.name 属性发生改变时候的通知.
从本质上来看 Person 给name赋值的时候 调用的是 setName 方法 ,无论 p1还是p2 调用的 setter 方法都是一样的,为什么 p1改变 name 属性值就能有通知, p2确没有,调用的 都是同一个 setName:(NSString *)name 方法,区别怎么那么大?
小编窥探尝试1
接下来小编打印下p1和p2的内存地址 看看p1和p2内存地址能不能一探究竟.
从 p1和 p2内存地址上也看不出来什么东东.
小编窥探尝试2 打印 p1和 p2 的 class 信息
what 什么 输出的 class 都是 Person 类 ,既然同一个类 同一个 setter 方法,为什么我们不一样呢?
小编窥探尝试3 打印 object_getClass 试试看 我们都知道object_getClass(id) 才会返回这个实例对象的真实 class 类型
什么 , 添加 KVO 之后说好的 Person 类跑哪去了, NSKVONotifying_Person是什么东东?
为了进一步窥探 KVO 添加前后的变化 小编窥探尝试4 打印 setName 方法实现IMP指针有没有发生改变,我们知道同一个方法的实现 IMP 地址是不变的.
连 setName方法都不一样了 , 为了一探究竟 小编绝对对上边的 NSKVONotifying_Person 和 添加 KVO 之后的 imp 指针进行进一步研究.
首先 在 lldb 上输入 imp1和 imp2
发生了 imp1 方法实现在 Foundation 框架里的 _NSSetObjectValueAndNotify 函数中 ,而 imp2 则调用了 Person setName 方法
也就是说添加了 KVO 之后 p1 修改 name 值之后 不再调用 Person 的 setName方法 ,而 p2没有添加 kvo 监听 依然正常调用 setName:方法 ,由此可以得出 p1 添加完 KVO 监听后 系统修改了默认方法实现,那么既然没有调用 setName: 方法 为什么 p1.name 的值也发生了改变?
接下来我们准备对刚才 NSKVONotifying_Person 类进行下一步研究, NSKVONotifying_Person 和 Person 有没有内在的联系呢?
小编窥探尝试5 NSKVONotifying_Person和 Person 之间的联系时什么
通过打印 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;
}
复制代码
从输出结果可以看出来 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 去告诉监听器属性值发生了改变 .
由于苹果 Foundation 框架是不开源的 ,所以我们依然可以通过重写Person 的 willChangeValueForKey 和 didChangeValueForKey 验证我们的猜想 .
首先当我们改变p1.name 的值时 并不是首先执行的 setName: 这个方法 ,而是先调用了 willChangeValueForKey 其次 调用父类的 setter 方法 对属性赋值 ,然后再调用 didChangeValueForKey 方法 ,并在 didChangeValueForKey 内部 调用监听器的 observeValueForKeyPath方法 告诉外界 属性值发生了改变.
至于重写了 dealloc 和 class 方法 是为了做一些 KVO 释放内存 和 隐藏外界对于 NSKVONotifying_Person 子类的存在
这就是我们调用 [p1 class] 和 [p2 class]结果都显示 Person 类 ,让我们误以为 Person 没有发生变化 补充说明 ,KVC 对属性赋值时候 是会在这个类里边 去查找 _age isAge setAge setIsAge 等方法的 ,最终会调用属性的 setter 方法 ,那么如果添加了 KVO 还是会被触发的 . 相反 设置成员变量 _age 由于不会触发 setter 方法 ,因此不会去触发 KVO 相关的代码 .
好了,我是大兵布莱恩特,欢迎加入博主技术交流群,iOS 开发交流群