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 指针交换/篡改)。
- 动态创建子类:当为一个对象注册 KVO 观察者时,Runtime 会在运行时动态创建一个该类的子类(通常命名为
NSKVONotifying_XXX)。 - 重写 Setter 方法:Runtime 会为这个动态子类重写被观察属性的 setter 方法。在重写的 setter 方法中,系统会在调用原始 setter 前后,自动插入
willChangeValueForKey:和didChangeValueForKey:方法,从而触发通知机制35。 - 修改 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 的查找顺序如下:
- 查找 Getter 方法:按
getKey->key->isKey->_key的顺序查找69。如果找到,直接调用该方法返回结果。 - 查找实例变量(Ivar) :如果没找到方法,且
+ (BOOL)accessInstanceVariablesDirectly返回YES(默认返回 YES),则按_key->_isKey->key->isKey的顺序直接读取内存中的实例变量。 - 兜底处理:如果都没找到,会调用
- (id)valueForUndefinedKey:(NSString *)key。默认实现会抛出异常(崩溃),开发者可以重写该方法进行容错处理6。
2. 设值流程(setValue:forKey:)
当调用 [obj setValue:value forKey:@"key"] 时,查找顺序如下:
- 查找 Setter 方法:按
setKey:->_setKey:的顺序查找68。如果找到,直接调用该方法传入值。 - 查找实例变量(Ivar) :如果没有 Setter 方法,且允许直接访问实例变量,则按
_key->_isKey->key->isKey的顺序查找。找到后直接将值赋给该实例变量。 - 兜底处理:如果都没找到,调用
- (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 将无法感知到属性的变化