对小码哥底层班视频学习的总结与记录. 面试题部分, 通过对面试题的分析探索问题的本质内容.
问题
iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么?)
如何手动触发 KVO
首先需要了解 KVO 基本使用, KVO 的全称 Key-Value Observing, 俗称 "键值监听", 可以用于监听某个对象属性值的改变.
- - (void)viewDidLoad {
- [super viewDidLoad];
- Person *p1 = [[Person alloc] init];
- Person *p2 = [[Person alloc] init];
- p1.age = 1;
- p1.age = 2;
- p2.age = 2;
- // self 监听 p1 的 age 属性
- NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
- [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
- p1.age = 10;
- [p1 removeObserver:self forKeyPath:@"age"];
- }
- - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
- {
- NSLog(@"监听到 %@的 %@改变了 %@", object, keyPath,change);
- }
- // 打印内容
监听到 < Person: 0x604000205460 > 的 age 改变了 {
- kind = 1;
- new = 10;
- old = 2;
- }
上述代码中可以看出, 在添加监听之后, age 属性的值在发生改变时, 就会通知到监听者, 执行监听者的 observeValueForKeyPath 方法.
探寻 KVO 底层实现原理
通过上述代码我们发现, 一旦 age 属性的值发生改变时, 就会通知到监听者, 并且我们知道赋值操作都是调用 set 方法, 我们可以来到 Person 类中重写 age 的 set 方法, 观察是否是 KVO 在 set 方法内部做了一些操作来通知监听者.
我们发现即使重写了 set 方法, p1 对象和 p2 对象调用同样的 set 方法, 但是我们发现 p1 除了调用 set 方法之外还会另外执行监听器的 observeValueForKeyPath 方法.
说明 KVO 在运行时获取对 p1 对象做了一些改变. 相当于在程序运行过程中, 对 p1 对象做了一些变化, 使得 p1 对象在调用 setage 方法的时候可能做了一些额外的操作, 所以问题出在对象身上, 两个对象在内存中肯定不一样, 两个对象可能本质上并不一样. 接下来来探索 KVO 内部是怎么实现的.
KVO 底层实现分析
首先我们对上述代码中添加监听的地方打断点, 看观察一下, addObserver 方法对 p1 对象做了什么处理? 也就是说 p1 对象在经过 addObserver 方法之后发生了什么改变, 我们通过打印 isa 指针如下图所示
通过上图我们发现, p1 对象执行过 addObserver 操作之后, p1 对象的 isa 指针由之前的指向类对象 Person 变为指向 NSKVONotifyin_Person 类对象, 而 p2 对象没有任何改变. 也就是说一旦 p1 对象添加了 KVO 监听以后, 其 isa 指针就会发生变化, 因此 set 方法的执行效果就不一样了.
那么我们先来观察 p2 对象在内容中是如何存储的, 然后对比 p2 来观察 p1. 首先我们知道, p2 在调用 setage 方法的时候, 首先会通过 p2 对象中的 isa 指针找到 Person 类对象, 然后在类对象中找到 setage 方法. 然后找到方法对应的实现. 如下图所示
但是刚才我们发现 p1 对象的 isa 指针在经过 KVO 监听之后已经指向了 NSKVONotifyin_Person 类对象, NSKVONotifyin_Person 其实是 Person 的子类, 那么也就是说其 superclass 指针是指向 Person 类对象的, NSKVONotifyin_Person 是 runtime 在运行时生成的. 那么 p1 对象在调用 setage 方法的时候, 肯定会根据 p1 的 isa 找到 NSKVONotifyin_Person, 在 NSKVONotifyin_Person 中找 setage 的方法及实现.
经过查阅资料我们可以了解到. NSKVONotifyin_Person 中的 setage 方法中其实调用了 Fundation 框架中 C 语言函数 _NSsetIntValueAndNotify,_NSsetIntValueAndNotify 内部做的操作相当于, 首先调用 willChangeValueForKey 将要改变方法, 之后调用父类的 setage 方法对成员变量赋值, 最后调用 didChangeValueForKey 已经改变方法. didChangeValueForKey 中会调用监听器的监听方法, 最终来到监听者的 observeValueForKeyPath 方法中.
那么如何验证 KVO 真的如上面所讲的方式实现?
首先经过之前打断点打印 isa 指针, 我们已经验证了, 在执行添加监听的方法时, 会将 isa 指针指向一个通过 runtime 创建的 Person 的子类 NSKVONotifyin_Person. 另外我们可以通过打印方法实现的地址来看一下 p1 和 p2 的 setage 的方法实现的地址在添加 KVO 前后有什么变化.
- // 通过 methodForSelector 找到方法实现的地址
- NSLog(@"添加 KVO 监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
- NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
- [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
- NSLog(@"添加 KVO 监听之后 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
我们发现在添加 KVO 监听之前, p1 和 p2 的 setAge 方法实现的地址相同, 而经过 KVO 监听之后, p1 的 setAge 方法实现的地址发生了变化, 我们通过打印方法实现来看一下前后的变化发现, 确实如我们上面所讲的一样, p1 的 setAge 方法的实现由 Person 类方法中的 setAge 方法转换为了 C 语言的 Foundation 框架的_NSsetIntValueAndNotify 函数.
Foundation 框架中会根据属性的类型, 调用不同的方法. 例如我们之前定义的 int 类型的 age 属性, 那么我们看到 Foundation 框架中调用的_NSsetIntValueAndNotify 函数. 那么我们把 age 的属性类型变为 double 重新打印一遍
我们发现调用的函数变为了_NSSetDoubleValueAndNotify, 那么这说明 Foundation 框架中有许多此类型的函数, 通过属性的不同类型调用不同的函数. 那么我们可以推测 Foundation 框架中还有很多例如
_NSSetBoolValueAndNotify,_NSSetCharValueAndNotify,_NSSetFloatValueAndNotify,_NSSetLongValueAndNotify
等等函数.
我们可以找到 Foundation 框架文件, 通过命令行查询关键字找到相关函数
NSKVONotifyin_Person 内部结构是怎样的?
首先我们知道, NSKVONotifyin_Person 作为 Person 的子类, 其 superclass 指针指向 Person 类, 并且 NSKVONotifyin_Person 内部一定对 setAge 方法做了单独的实现, 那么 NSKVONotifyin_Person 同 Person 类的差别可能就在于其内存储的对象方法及实现不同. 我们通过 runtime 分别打印 Person 类对象和 NSKVONotifyin_Person 类对象内存储的对象方法
- - (void)viewDidLoad {
- [super viewDidLoad];
- Person *p1 = [[Person alloc] init];
- p1.age = 1.0;
- Person *p2 = [[Person alloc] init];
- p1.age = 2.0;
- // self 监听 p1 的 age 属性
- NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
- [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
- [self printMethods: object_getClass(p2)];
- [self printMethods: object_getClass(p1)];
- [p1 removeObserver:self forKeyPath:@"age"];
- }
- - (void) printMethods:(Class)cls
- {
- unsigned int count ;
- Method *methods = class_copyMethodList(cls, &count);
- NSMutableString *methodNames = [NSMutableString string];
- [methodNames appendFormat:@"%@ -", cls];
- for (int i = 0 ; i < count; i++) {
- Method method = methods[i];
- NSString *methodName = NSStringFromSelector(method_getName(method));
- [methodNames appendString: methodName];
- [methodNames appendString:@" "];
- }
- NSLog(@"%@",methodNames);
- free(methods);
- }
上述打印内容如下
通过上述代码我们发现 NSKVONotifyin_Person 中有 4 个对象方法. 分别为 setAge: class dealloc _isKVOA, 那么至此我们可以画出 NSKVONotifyin_Person 的内存结构以及方法调用顺序.
这里 NSKVONotifyin_Person 重写 class 方法是为了隐藏 NSKVONotifyin_Person. 不被外界所看到. 我们在 p1 添加过 KVO 监听之后, 分别打印 p1 和 p2 对象的 class 可以发现他们都返回 Person.
- NSLog(@"%@,%@",[p1 class],[p2 class]);
- // 打印结果 Person,Person
如果 NSKVONotifyin_Person 不重写 class 方法, 那么当对象要调用 class 对象方法的时候就会一直向上找来到 nsobject, 而 nsobect 的 class 的实现大致为返回自己 isa 指向的类, 返回 p1 的 isa 指向的类那么打印出来的类就是 NSKVONotifyin_Person, 但是 apple 不希望将 NSKVONotifyin_Person 类暴露出来, 并且不希望我们知道 NSKVONotifyin_Person 内部实现, 所以在内部重写了 class 类, 直接返回 Person 类, 所以外界在调用 p1 的 class 对象方法时, 是 Person 类. 这样 p1 给外界的感觉 p1 还是 Person 类, 并不知道 NSKVONotifyin_Person 子类的存在.
那么我们可以猜测 NSKVONotifyin_Person 内重写的 class 内部实现大致为
- - (Class) class {
- // 得到类对象, 在找到类对象父类
- return class_getSuperclass(object_getClass(self));
- }
验证 didChangeValueForKey: 内部会调用 observer 的 observeValueForKeyPath:ofObject:change:context: 方法
我们在 Person 类中重写 willChangeValueForKey: 和 didChangeValueForKey: 方法, 模拟他们的实现.
- - (void)setAge:(int)age
- {
- NSLog(@"setAge:");
- _age = age;
- }
- - (void)willChangeValueForKey:(NSString *)key
- {
- NSLog(@"willChangeValueForKey: - begin");
- [super willChangeValueForKey:key];
- NSLog(@"willChangeValueForKey: - end");
- }
- - (void)didChangeValueForKey:(NSString *)key
- {
- NSLog(@"didChangeValueForKey: - begin");
- [super didChangeValueForKey:key];
- NSLog(@"didChangeValueForKey: - end");
- }
再次运行来查看 didChangeValueForKey 的方法内运行过程, 通过打印内容可以看到, 确实在 didChangeValueForKey 方法内部已经调用了 observer 的 observeValueForKeyPath:ofObject:change:context: 方法.
回答问题:
iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么?) 答. 当一个对象使用了 KVO 监听, iOS 系统会修改这个对象的 isa 指针, 改为指向一个全新的通过 Runtime 动态创建的子类, 子类拥有自己的 set 方法实现, set 方法实现内部会顺序调用 willChangeValueForKey 方法, 原来的 setter 方法实现, didChangeValueForKey 方法, 而 didChangeValueForKey 方法内部又会调用监听器的 observeValueForKeyPath:ofObject:change:context: 监听方法.
如何手动触发 KVO 答. 被监听的属性的值被修改时, 就会自动触发 KVO. 如果想要手动触发 KVO, 则需要我们自己调用 willChangeValueForKey 和 didChangeValueForKey 方法即可在不改变属性值的情况下手动触发 KVO, 并且这两个方法缺一不可.
通过以下代码可以验证
- Person *p1 = [[Person alloc] init];
- p1.age = 1.0;
- NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
- [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
- [p1 willChangeValueForKey:@"age"];
- [p1 didChangeValueForKey:@"age"];
- [p1 removeObserver:self forKeyPath:@"age"];
通过打印我们可以发现, didChangeValueForKey 方法内部成功调用了 observeValueForKeyPath:ofObject:change:context:, 并且 age 的值并没有发生改变.
文中如果有不对的地方欢迎指出. 我是 xx_cc, 一只长大很久但还没有二够的家伙.
来源: https://juejin.im/post/5adab70cf265da0b736d37a8