KVC 与 KVO
问题
KVC 和 KVO 的底层原理是什么?KVO 是如何通过 isa-swizzling 实现的?
答案
KVC(Key-Value Coding)
通过字符串键名访问对象属性:
Person *p = [[Person alloc] init];
// 常规方式
p.name = @"Alice";
// KVC 方式
[p setValue:@"Alice" forKey:@"name"];
NSString *name = [p valueForKey:@"name"];
// 访问嵌套属性
[p setValue:@"Beijing" forKeyPath:@"address.city"];
KVC 查找过程
setValue:forKey: 的查找顺序:
valueForKey: 的查找顺序:
- 按顺序查找 getter:
getName→name→isName→_name - 未找到 → 查找集合方法(
countOfName、objectInNameAtIndex:等) - 仍未找到 → 查找
accessInstanceVariablesDirectly - 返回 YES → 按顺序查找实例变量:
_name→_isName→name→isName - 仍未找到 →
valueForUndefinedKey:
KVO(Key-Value Observing)
观察对象属性变化的机制:
// 注册观察
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
// 回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"age"]) {
NSLog(@"age 从 %@ 变为 %@",
change[NSKeyValueChangeOldKey],
change[NSKeyValueChangeNewKey]);
}
}
// 移除观察(必须!否则崩溃)
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
}
KVO 底层原理:isa-swizzling
添加观察者后,Runtime 做了以下工作:
- 动态创建子类
NSKVONotifying_Person(继承自Person) - 修改对象的 isa 指针指向新子类
- 重写 setter 方法,添加通知逻辑:
// NSKVONotifying_Person 的 setAge: 实现(伪代码)
- (void)setAge:(int)age {
[self willChangeValueForKey:@"age"];
[super setAge:age]; // 调用原始 setter
[self didChangeValueForKey:@"age"];
}
// 同时重写了以下方法来隐藏中间子类的存在
- (Class)class { return [Person class]; } // 伪装
- (BOOL)_isKVOA { return YES; }
- (void)dealloc { /* 清理逻辑 */ }
验证 isa-swizzling
Person *p = [[Person alloc] init];
NSLog(@"%@", object_getClass(p)); // Person
[p addObserver:self forKeyPath:@"name" options:0 context:nil];
NSLog(@"%@", object_getClass(p)); // NSKVONotifying_Person
NSLog(@"%@", [p class]); // Person(被伪装了)
手动触发 KVO
直接修改实例变量(不通过 setter)不会触发 KVO。需手动触发:
// 手动触发
[self willChangeValueForKey:@"age"];
_age = 20; // 直接修改实例变量
[self didChangeValueForKey:@"age"];
// 禁止自动 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO; // 关闭 age 的自动通知
}
return [super automaticallyNotifiesObserversForKey:key];
}
依赖键
当一个属性依赖多个其他属性时:
// fullName 依赖 firstName 和 lastName
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}
// 修改 firstName 或 lastName 都会触发 fullName 的 KVO
常见面试问题
Q1: KVC 会触发 KVO 吗?
答案:会。KVC 的 setValue:forKey: 内部会调用 willChangeValueForKey:/didChangeValueForKey:,所以即使没有走 setter 也会触发 KVO。
Q2: KVO 为什么要在 dealloc 中移除观察者?
答案:对象销毁后如果还有观察关系存在,被观察者属性变化时会向已释放的观察者发消息,导致 EXC_BAD_ACCESS 崩溃。iOS 11+ 后系统做了部分安全处理,但仍建议手动移除。
Q3: 如何实现 KVO 的安全移除?
答案:
// 方案 1:用 context 区分
static void *MyContext = &MyContext;
[obj addObserver:self forKeyPath:@"value"
options:NSKeyValueObservingOptionNew context:MyContext];
// 方案 2:使用第三方安全方案(如 FBKVOController)
[self.KVOController observe:obj keyPath:@"value"
options:NSKeyValueObservingOptionNew
block:^(id observer, id object, NSDictionary *change) {
// 自动管理生命周期
}];
Q4: 直接修改成员变量(不走 setter)能否触发 KVO?
答案:不能。KVO 依赖 willChangeValueForKey: / didChangeValueForKey: 方法,这些是在重写的 setter 中调用的。直接通过 _age = 20 修改实例变量绕过了 setter,不会触发 KVO。需要手动调用 will/didChangeValueForKey: 来触发。