跳到主要内容

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: 的查找顺序

  1. 按顺序查找 getter:getNamenameisName_name
  2. 未找到 → 查找集合方法(countOfNameobjectInNameAtIndex: 等)
  3. 仍未找到 → 查找 accessInstanceVariablesDirectly
  4. 返回 YES → 按顺序查找实例变量:_name_isNamenameisName
  5. 仍未找到 → 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 做了以下工作:

  1. 动态创建子类 NSKVONotifying_Person(继承自 Person
  2. 修改对象的 isa 指针指向新子类
  3. 重写 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: 来触发。

相关链接