🎯 KVO:动态子类化与 ISA 交换
KVO (Key-Value Observing) 的本质,是系统在运行时动态创建子类,并通过 ISA 交换(ISA-Swizzling) 技术,让被观察对象“偷偷”变成这个动态子类的实例,从而在属性 setter 方法中插入通知逻辑。
-
动态创建子类:当为对象
person的属性name注册第一个观察者时,系统会在运行时动态创建一个名为NSKVONotifying_Person的子类,并让它继承自Person类。 -
-
重写被观察属性的 Setter:这是最核心的一步。重写后的
setName:方法,其内部实现逻辑如下:objc
// 伪代码示例 - (void)setName:(NSString *)newName { [self willChangeValueForKey:@"name"]; // 1. 通知观察者:值即将改变 [super setName:newName]; // 2. 调用父类(原类)的setter,真正修改值 [self didChangeValueForKey:@"name"]; // 3. 通知观察者:值已经改变 } -
重写
class方法:为了让外部代码(如调试器)以为对象还是原来的Person类,动态子类会重写class方法,返回[Person class],从而隐藏其真实类型。
-
-
执行 ISA 交换:将原
person对象的isa指针,从指向Person类,修改为指向这个新创建的NSKVONotifying_Person类。 -
触发通知流程:当后续修改
person.name时,会调用动态子类被重写的setName:方法,从而自动触发willChangeValueForKey:和didChangeValueForKey:。在didChangeValueForKey:内部,会调用观察者的observeValueForKeyPath:ofObject:change:context:方法,将新旧值传递给观察者。
重要注意事项:KVO 的本质是监听 setter 方法,因此直接修改成员变量(即使用
_name = newName)不会触发 KVO。
🔑 KVC:按模式搜索的访问机制
KVC (Key-Value Coding) 的核心原理是标准的搜索模式。当调用 setValue:forKey: 或 valueForKey: 时,系统会按照一套预定义的、可预测的规则来搜索并完成操作。
setValue:forKey: 的底层执行逻辑
-
查找 Setter 方法:
优先查找set<Key>:,其次查找_set<Key>:。一旦找到,直接调用该方法并结束流程。 -
检查访问权限:
若未找到 Setter 方法,KVC 会检查accessInstanceVariablesDirectively方法的返回值(默认为YES)。- 若为
YES,允许直接访问成员变量(Ivar)。 - 若为
NO,则直接进入第4步。
- 若为
-
查找成员变量:
按以下顺序查找成员变量,一旦找到,直接赋值并结束流程。_<key>_is<Key><key>is<Key>
-
异常处理:
若上述步骤均未找到,最后会调用setValue:forUndefinedKey:,该方法默认会抛出NSUndefinedKeyException异常。
valueForKey: 的底层执行逻辑
-
查找 Getter 方法:
按以下顺序查找方法,找到则调用并返回值。get<Key><key>is<Key>_<key>
-
查找集合方法:
如果第一步未找到,KVC 会检查属性是否是集合类型。它会查找countOf<Key>和objectIn<Key>AtIndex:等方法,如果找到,则返回一个可以响应NSArray方法的代理对象。 -
查找成员变量:
若前两步均未找到,KVC 会直接查找成员变量(如_<key>、<key>),找到则直接返回其值。 -
异常处理:
若所有步骤都失败,最后会调用valueForUndefinedKey:,该方法默认抛出异常。
🗺️ KVO 与 KVC 的关系
KVO 和 KVC 紧密协作。KVC 提供了通过字符串 key 访问属性的能力,而 KVO 正是依赖于此。最直接的证明就是:通过 KVC 修改属性(如 [person setValue:@"New Name" forKey:@"name"]),会完整地触发 KVO 通知。因为 KVC 在内部执行时,依然会经过 setter 方法的查找和调用流程。