四.iOS KVO&KVC

4 阅读6分钟

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 自动处理类型转换:NSNumberNSIntegerCGFloat
  • 集合操作符:@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
  • 需要确保 addObserverremoveObserver 配对,或使用 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 配对

动手实践

  1. class_getInstanceMethod 验证 KVO 前后对象的 isa 地址变化
  2. 实现一个简单的 KVOController 管理观察者生命周期
  3. 写一个会导致 KVO crash 的例子并修复

高级(P2)— 封装和替代方案

  • 能设计安全易用的 KVO 封装组件
  • 理解 Combine / ReactiveSwift 与 KVO 的关系和区别
  • 能制定团队 KVO 使用规范
  • 理解 KVO 替代方案(delegate / Notification / Combine / async-await)

实战项目

  1. 将项目中所有 KVO 替换为 Combine 或基于 Block 的安全封装
  2. 实现一个自动移除 KVO 的基础类
  3. 设计一个基于 KVO 的状态驱动 UI 框架