iOS 底层原理 KVO KVC

2 阅读4分钟

KVO基本使用

下面代码只有age的监听被触发了。

@interface Person : NSObject{
    double height;
}
@property (nonatomic, assign) int age;
- (void)changeHeight:(double)newheihgt;
@end

@implementation Person
- (void)changeHeight:(double)newheihgt{
    height = newheihgt;
}
@end


#import "Person.h"
@interface ViewController (){
    int count;
}
@property (nonatomic, strong) Person *p;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.p = [[Person alloc] init];
    self.p.age = 100;
    
    [self.p addObserver:self forKeyPath:@"age"
              options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    [self.p addObserver:self forKeyPath:@"height"
              options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    [self addObserver:self forKeyPath:@"count"
              options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    
}

- (void)dealloc
{
    [self.p removeObserver:self forKeyPath:@"age"];
    [self.p removeObserver:self forKeyPath:@"height"];
    [self removeObserver:self forKeyPath:@"count"];
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.p.age = 101;
    [self.p changeHeight:180];
    count = 1;
    
    
}

// 如果注册了监听,这里不实现监听的回调会crash.
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@ %@",keyPath, change);
    
}

监听的如果是基本数据类型,比如int,double,struct等。则返回的监听结果被包装了。

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@ %@",keyPath, change);
    id newValue = change[NSKeyValueChangeNewKey];
    if ([newValue isKindOfClass:[NSNumber class]]) {
        NSLog(@"✅ 证明成功:新值是一个 NSNumber 对象,具体值为: %@", newValue);
    } else {
        NSLog(@"❌ 证明失败:新值不是 NSNumber 对象");
    }
    
}

如果要实现监听,可以手动修改代码

- (void)changeHeight:(double)newheihgt{
    [self willChangeValueForKey:@"height"]; // 1. 手动通知:即将改变
    height = newheihgt;                     // 2. 直接修改实例变量
    [self didChangeValueForKey:@"height"];  // 3. 手动通知:已经改变
}

KVO的原理

KVO 的底层实现依赖于 Objective-C 强大的 Runtime 机制,核心技术被称为 isa-swizzling(isa 指针交换/篡改)。

  1. 动态创建子类:当为一个对象注册 KVO 观察者时,Runtime 会在运行时动态创建一个该类的子类(通常命名为 NSKVONotifying_XXX)。
  2. 重写 Setter 方法:Runtime 会为这个动态子类重写被观察属性的 setter 方法。在重写的 setter 方法中,系统会在调用原始 setter 前后,自动插入 willChangeValueForKey: 和 didChangeValueForKey: 方法,从而触发通知机制35。
  3. 修改 isa 指针:Runtime 会将原对象的 isa 指针修改为指向这个动态生成的子类。这样一来,当外部调用 setter 方法时,实际上调用的是子类重写后的方法,从而实现了无侵入式的拦截

KVC

KVC 提供了一套标准的 API,主要包含取值和设值两大类操作:

1. 基础取值与设值

  • 取值:- (id)valueForKey:(NSString *)key;
  • 设值:- (void)setValue:(id)value forKey:(NSString *)key;

2. 键路径(KeyPath)访问
当需要访问嵌套对象的属性时,可以使用点语法拼接路径(如 @"address.street"),KVC 会逐层递归解析7。

  • 取值:- (id)valueForKeyPath:(NSString *)keyPath;

  • 设值:- (void)setValue:(id)value forKeyPath:(NSString *)keyPath; 3. 集合操作符(Collection Operators)
    KVC 内置了强大的聚合计算能力,可以直接对 NSArray 等集合类进行统计,无需手动遍历:

  • @count:元素个数

  • @sum、@avg:总和、平均值

  • @max、@min:最大值、最小值

  • 示例:[array valueForKeyPath:@"@avg.salary"] 可直接获取数组中对象的平均薪资。

4. 字典与模型互转

  • 字典转模型:- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;(常用于 JSON 解析)
  • 模型转字典:- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

KVC原理

KVC 的底层实现原理

KVC 的强大和灵活性完全建立在 Objective-C 的 Runtime(运行时)机制之上。当调用 KVC 方法时,系统会按照极其严格的搜索规则去查找属性或方法69。

1. 取值流程(valueForKey:)

当调用 [obj valueForKey:@"key"] 时,Runtime 的查找顺序如下:

  1. 查找 Getter 方法:按 getKey -> key -> isKey -> _key 的顺序查找69。如果找到,直接调用该方法返回结果。
  2. 查找实例变量(Ivar) :如果没找到方法,且 + (BOOL)accessInstanceVariablesDirectly 返回 YES(默认返回 YES),则按 _key -> _isKey -> key -> isKey 的顺序直接读取内存中的实例变量。
  3. 兜底处理:如果都没找到,会调用 - (id)valueForUndefinedKey:(NSString *)key。默认实现会抛出异常(崩溃),开发者可以重写该方法进行容错处理6。
2. 设值流程(setValue:forKey:)

当调用 [obj setValue:value forKey:@"key"] 时,查找顺序如下:

  1. 查找 Setter 方法:按 setKey: -> _setKey: 的顺序查找68。如果找到,直接调用该方法传入值。
  2. 查找实例变量(Ivar) :如果没有 Setter 方法,且允许直接访问实例变量,则按 _key -> _isKey -> key -> isKey 的顺序查找。找到后直接将值赋给该实例变量。
  3. 兜底处理:如果都没找到,调用 - (void)setValue:(id)value forUndefinedKey:(NSString *)key,默认抛出异常6。
3. 自动装箱与拆箱(Autoboxing)

KVC 是 OC 中极少数支持自动装箱的机制。当你通过 KVC 给一个 int 类型的属性赋值时,传入 NSNumber 对象,KVC 底层会自动将其拆箱为 int 并写入内存;反之,取值时也会自动将 int 包装成 NSNumber 返回

KVC 与 KVO 的底层联系

KVC 是 KVO(键值观察)的基石。正如我们之前讨论的,KVO 的底层是通过 isa-swizzling 动态生成子类并重写 Setter 方法来实现的。

这意味着:只有通过 KVC 兼容的方式(即调用 Setter 方法或 setValue:forKey:)修改属性,KVO 才会被触发。如果你直接修改底层的实例变量(如 obj->_age = 18;),由于绕过了 KVC 的 Setter 拦截,KVO 将无法感知到属性的变化