KVC / KVO
面试回答版
一句话概括
KVC(Key-Value Coding)是通过字符串键路径访问对象属性的机制;KVO(Key-Value Observing)是基于 KVC 的属性变化通知机制。两者都是 Objective-C Runtime 的动态能力。
KVC(Key-Value Coding)
工作原理
调用 [obj setValue:val forKey:@"name"] 时的查找顺序:
setValue:forKey:
│
├─ 1. 查找 setName: 方法(set<Key> 模式)
│ 调用 setter 并附带值
│
└─ 2. setter 未找到 → lookupSetValue: 继续
│
├─ 2.1 检查 +accessInstanceVariablesDirectly 是否返回 YES(默认 YES)
│
├─ 2.2 按顺序查找实例变量:
│ _name, _isName, name, isName
│ 找到后直接赋值 ivar
│
└─ 2.3 变量也未找到 → setValue:forUndefinedKey:
(默认抛 NSUnknownKeyException)
调用 [obj valueForKey:@"name"] 的查找顺序:
valueForKey:
│
├─ 1. 查找 getName: / name: / isName: / _name: 方法(按此顺序)
│ 调用 getter 并返回值
│
├─ 2. getter 未找到 → 检查 +accessInstanceVariablesDirectly
│
├─ 3. 按顺序查找实例变量:
│ _name, _isName, name, isName
│
└─ 4. 都未找到 → valueForUndefinedKey:
KVC 处理非对象类型
setValue:forKey:传入nil对应基本类型时 →setNilValueForKey:(默认抛 NSInvalidArgumentException)- KVC 自动处理类型转换:
NSNumber→NSInteger、CGFloat等 - 集合操作符:
@avg、@count、@max、@min、@sum、@distinctUnionOfObjects
// KVC 聚合操作符
NSArray *items = @[@{@"price": @10}, @{@"price": @20}, @{@"price": @30}];
NSNumber *avgPrice = [items valueForKeyPath:@"@avg.price"]; // @20
NSNumber *maxPrice = [items valueForKeyPath:@"@max.price"]; // @30
NSArray *prices = [items valueForKeyPath:@"@distinctUnionOfObjects.price"]; // @[@10, @20, @30]
KVO(Key-Value Observing)
底层原理
// 调用 addObserver:forKeyPath:options:context: 时
[obj addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
// Runtime 做的事情:
// 1. 动态创建一个新类,继承原类,类名为 NSKVONotifying_<OriginalClass>
// 2. 在新类中重写被观察属性的 setter 方法
// 3. 通过 isa-swizzling 将 obj 的 isa 指针指向新类
// 4. 在新 setter 中插入 willChangeValueForKey: 和 didChangeValueForKey: 调用
生成的新 setter 等效:
// NSKVONotifying_OriginalClass 中重写的 setter
- (void)setStatus:(NSString *)status {
[self willChangeValueForKey:@"status"];
[super setStatus:status]; // 调用原始类的 setter
[self didChangeValueForKey:@"status"];
}
// didChangeValueForKey: 内部会调用 observeValueForKeyPath:ofObject:change:context:
KVO 的自动/手动控制
// 1. 关闭某个属性的自动通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"status"]) {
return NO; // 手动控制
}
return [super automaticallyNotifiesObserversForKey:key];
}
// 2. 手动触发通知
- (void)updateStatusManually:(NSString *)newStatus {
[self willChangeValueForKey:@"status"];
_status = newStatus;
[self didChangeValueForKey:@"status"];
}
KVO 依赖键
当一个属性的变化依赖于其他属性时,需要声明依赖关系:
// fullName 依赖于 firstName 和 lastName
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}
// 或者使用更动态的方式
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
keyPaths = [keyPaths setByAddingObjectsFromSet:
[NSSet setWithObjects:@"firstName", @"lastName", nil]];
}
return keyPaths;
}
安全隐患与对策
1. 重复移除观察者导致 Crash
// ⚠️ 重复 removeObserver:forKeyPath: 会抛 NSException
@try {
[obj removeObserver:self forKeyPath:@"status"];
} @catch (NSException *e) {
// 不要空 catch,应该避免重复移除
}
// ✓ 推荐:统一 context 标记 + add/remove 成对出现
static void *kStatusContext = &kStatusContext;
- (void)startObserving {
[obj addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:kStatusContext];
}
- (void)stopObserving {
[obj removeObserver:self forKeyPath:@"status" context:kStatusContext];
}
2. dealloc 时未移除观察者
- (void)dealloc {
// iOS 9+ 在 dealloc 中会自动移除(但强烈建议显式移除)
[obj removeObserver:self forKeyPath:@"status"];
}
3. 被观察对象先于观察者释放
- KVO 不会持有被观察对象,被观察对象释放后观察者仍持有 kvo 注册记录。
- 如果被观察对象在释放时未移除观察者,会触发
warning: KVO for XXX was still registered。 - 需要确保
addObserver和removeObserver配对,或使用 KVOController 等封装。
context 的正确使用
// 错误:无 context,子类父类同时监听同一 keyPath 时冲突
[obj addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
// 正确:用静态变量作 context,避免冲突
static void *kMyContext = &kMyContext;
[obj addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:kMyContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if (context == kMyContext) {
// 自己的处理逻辑
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
高频追问清单
| 问题 | 关键要点 |
|---|---|
| KVC 的查找顺序是什么? | setter → ivar → forUndefinedKey |
| KVO 底层如何实现? | isa-swizzling,创建 NSKVONotifying_XXX 中间类重写 setter |
| KVO 是否会触发 setter? | 必须通过 setter 修改才会触发(直接修改 ivar 不会触发 KVO) |
| 如何手动触发 KVO? | willChangeValueForKey: + didChangeValueForKey: |
| KVO 的自动通知可以关闭吗? | automaticallyNotifiesObserversForKey: 返回 NO |
| 重复 removeObserver 为什么会 crash? | KVO 是 C 数组实现的注册表,找不到对应记录会抛异常 |
| KVO context 的作用? | 区分父类/子类对同一 keyPath 的监听,避免冲突 |
| KVO 支持集合变更通知吗? | 支持,需要实现 mutableArrayValueForKey: / mutableSetValueForKey: |
项目落地版
场景 1:KVC 字典转 Model(安全版本)
@interface Model : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *email;
@end
@implementation Model
+ (instancetype)modelWithDict:(NSDictionary *)dict {
Model *model = [[Model alloc] init];
[dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
// 检查属性是否存在,避免 setValue:forUndefinedKey: 崩溃
objc_property_t prop = class_getProperty([Model class], key.UTF8String);
if (prop != NULL) {
[model setValue:value forKey:key];
}
}];
return model;
}
// 处理字典中存在但 Model 中没有的 key
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
// 静默忽略,或记录日志
NSLog(@"[KVC] 忽略未定义的 key: %@", key);
}
@end
场景 2:KVO 安全封装(KVOController 模式)
// KVOController — 自动管理观察者生命周期
@interface KVOController : NSObject
@property (nonatomic, weak) id observer;
- (void)observe:(id)target keyPath:(NSString *)keyPath block:(void(^)(id newValue))block;
@end
@implementation KVOController {
NSMutableDictionary<NSString *, void(^)(id)> *_blocks;
}
- (instancetype)initWithObserver:(id)observer {
if (self = [super init]) {
_observer = observer;
_blocks = [NSMutableDictionary dictionary];
}
return self;
}
- (void)observe:(id)target keyPath:(NSString *)keyPath block:(void(^)(id))block {
_blocks[keyPath] = [block copy];
[target addObserver:self forKeyPath:keyPath
options:NSKeyValueObservingOptionNew
context:(__bridge void *)keyPath];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
void(^block)(id) = _blocks[keyPath];
if (block) {
block(change[NSKeyValueChangeNewKey]);
}
}
- (void)dealloc {
// 自动移除所有观察(但需要知道 target)
// 实际实现可通过 weak reference map 追踪所有注册
}
@end
场景 3:KVO 配合计算属性(依赖键)
@interface OrderViewModel : NSObject
@property (nonatomic) NSInteger unitPrice;
@property (nonatomic) NSInteger quantity;
@property (nonatomic, readonly) NSInteger totalPrice;
@end
@implementation OrderViewModel
+ (NSSet<NSString *> *)keyPathsForValuesAffectingTotalPrice {
return [NSSet setWithObjects:@"unitPrice", @"quantity", nil];
}
- (NSInteger)totalPrice {
return self.unitPrice * self.quantity;
}
@end
// 监听 totalPrice — 当 unitPrice 或 quantity 变化时都会触发
[orderViewModel addObserver:self forKeyPath:@"totalPrice" options:NSKeyValueObservingOptionNew context:nil];
场景 4:集合类型 KVO
@interface ListViewModel : NSObject
@property (nonatomic, strong) NSMutableArray<NSString *> *items;
@end
@implementation ListViewModel
- (instancetype)init {
if (self = [super init]) {
_items = [NSMutableArray array];
}
return self;
}
@end
// 要触发 KVO 通知,必须使用 mutableArrayValueForKey:
ListViewModel *vm = [ListViewModel new];
[[vm mutableArrayValueForKey:@"items"] addObject:@"new item"];
// 这样才会触发 KVO 通知(willChange/didChange)
场景 5:Swizzle KVO 实现(原理验证)
// dealloc 时自动移除 KVO 观察者
#import <objc/runtime.h>
@implementation NSObject (KVOAutoRemove)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method original = class_getInstanceMethod(self, @selector(dealloc));
Method swizzled = class_getInstanceMethod(self, @selector(kvoAutoRemove_dealloc));
method_exchangeImplementations(original, swizzled);
});
}
- (void)kvoAutoRemove_dealloc {
// 在 dealloc 时移除所有 KVO(作为最后保底)
// 注意:此处需要知道所有注册过的 observer(略)
// 建议使用 FBKVOController 等成熟方案
[self kvoAutoRemove_dealloc];
}
@end
学习路径与优先级
初级(P1)— 会用 KVC 和 KVO
- 掌握 KVC 基本赋值和取值(setValue:forKey: / valueForKey:)
- 掌握 KVO 基本监听(addObserver / observeValueForKeyPath / removeObserver)
- 理解
setValue:forUndefinedKey:的作用 - 知道 KVO 的
observeValueForKeyPath:ofObject:change:context:方法的完整签 - 知道在 dealloc 中移除 KVO 观察者
自检:
- 用 KVC 给嵌套对象属性赋值(keyPath)
- 用 KVO 监听一个属性的变化
中级(P0)— 理解原理,避免崩溃
- 理解 KVC 完整的 setter/getter/ivar 查找顺序
- 理解 KVO 的 isa-swizzling 机制
- 掌握 context 的正确使用场景
- 理解
automaticallyNotifiesObserversForKey的用法 - 理解 KVO 依赖键配置
- 掌握集合类型 KVO 的使用方式
- 能安全处理 KVO 的 add/remove 配对
动手实践:
- 用
class_getInstanceMethod验证 KVO 前后对象的 isa 地址变化 - 实现一个简单的 KVOController 管理观察者生命周期
- 写一个会导致 KVO crash 的例子并修复
高级(P2)— 封装和替代方案
- 能设计安全易用的 KVO 封装组件
- 理解 Combine / ReactiveSwift 与 KVO 的关系和区别
- 能制定团队 KVO 使用规范
- 理解 KVO 替代方案(delegate / Notification / Combine / async-await)
实战项目:
- 将项目中所有 KVO 替换为 Combine 或基于 Block 的安全封装
- 实现一个自动移除 KVO 的基础类
- 设计一个基于 KVO 的状态驱动 UI 框架