KVC
kvc全称是 key value coding,又称“键值编码”,可以通过key获取或修改其对应值,因此会破坏面向对象思想。
它提供一种机制可以间接访问对象的属性,而不是通过setter/getter方法。
常见API
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
key和keyPah的区别
key:仅用于直接访问对象的属性,不能用于访问嵌套的属性或集合属性的元素。
keyPath:除了可以访问属性,还可以访问嵌套的属性或集合属性的元素。
@interface HFPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) HFAnimal *animal;
@end
@interface HFAnimal : NSObject
@property (nonatomic, copy) NSString *type;
@end
HFPerson *person = [[HFPerson alloc] init];
[person setValue:@"John" forKey:@"name"]; // 使用 setValue:forKey: 设置 name 属性
[person setValue:@30 forKey:@"age"]; // 使用 setValue:forKey: 设置 age 属性
HFAnimal *animal = [[HFAnimal alloc] init];
animal.type = @"Cat";
person.animal = animal; // 设置 animal 属性
NSString *name = [person valueForKey:@"name"]; // 使用 valueForKey: 获取 name 属性值
NSInteger age = [[person valueForKey:@"age"] integerValue]; // 使用 valueForKey: 获取 age 属性值
NSString *type = [person valueForKeyPath:@"animal.type"]; // 使用 valueForKeyPath: 获取嵌套属性值
NSString *name = [person valueForKey:@"name"]; // 使用 valueForKey: 获取 name 属性值
NSString *type = [person valueForKeyPath:@"animal.type"]; // 使用 valueForKeyPath: 获取嵌套属性值
可以直观的发现key和keyPath的不同。
KVC原理
1、-(void)setValue:(id)value forKey:(NSString *)key;
首先按照setKey,_setKey的顺序查找setter方法,找到方法则直接调用并赋值
若未找到方法,则调用+(BOOL)accessInstanceVariablesDirectly方法判断,是否可以直接访问成员变量,默认YES。
若accessInstanceVariablesDirectly返回YES,则按照_key、_isKey、key、_isKey的顺序查找成员变量,找到直接赋值,未找到或accessInstanceVariablesDirectly返回NO抛出NSUnknownKeyException异常就会调用setValue:forUndefinedKey:并抛出NSUnknownKeyException异常。
2、-(nullable id)valueForKey:(NSString *)key
首先会按照getKey、key、isKey、_key的顺序找到getter方法,若找到直接调用取值。
若未找到,则调用+(BOOL)accessInstanceVariablesDirectly方法判断,是否可以直接访问成员变量,默认YES。
若返回YES,则按照_key、_iskey、key、iskey的顺序匹配实例变量。如果找到这样的实例变量,则返回接收器中实例变量的值,若未找到或返回NO则调用-valueForUndefinedKey:,并抛出NSUnknownKeyException异常。
[图片上传中...(836c8db8b5bc1f415847a8628e1c79ce.png-39928c-1713449155581-0)]
注意事项
key的值必须正确,如果拼写错误,就会出现异常。
当key的值是未定义的,会调用valueForUndefinedKey,如果重写了这个方法,key的值出错的时候会调用到此处。因为类可以反复嵌套,所以有keyPath,用路径实现访问。
可以通过kvc访问私有成员变量/属性。
如果参数类型不是对象指针类型,但值为nil,则调用setNilValueForKey:,-setNilValueForKey:的默认实现引发NSInvalidArgumentException,我们可以重写setNilValueForKey避免非对象传递nil出现的错误。对象传递nil不会调用此方法,会直接报错。
处理非对象,setValue时,如果要赋值的是基本数据类型,需要封装成NSNumber或NSValue类型,valueForKey时,返回的是id类型的对象,基本数据类型也会被封装成NSNumber或NSValue类型。valueForKey可以自动将值封装成对象,但是setValye不行,必须手动转换再执行setValue。
KVC的更多应用场景
1、批量操作
批量存值:dictionaryWithValuesForKeys
批量赋值:setValuesForKeysWithDictionary
HFPerson *person = [[HFPerson alloc] init];
[person setValue:@"John" forKey:@"name"]; // 使用 setValue:forKey: 设置 name 属性
[person setValue:@30 forKey:@"age"]; // 使用 setValue:forKey: 设置 age 属性
NSDictionary *dicOne = [person dictionaryWithValuesForKeys:@[@"name",@"age"]];
NSLog(@"dicOne = %@",dicOne);
NSDictionary *dicTwo = @{@"name":@"Mike",@"age":@35};
HFPerson *personTwo = [[HFPerson alloc] init];
[personTwo setValuesForKeysWithDictionary:dicTwo];
NSLog(@"personTwo.name = %@, age = %ld",personTwo.name,personTwo.age);
输出结果
dicOne = {
age = 30;
name = John;
}
personTwo.name = Mike, age = 35
2、字典模型互转
如果模型和dic不匹配
字典转模型:重写setValue:forUndefinedKey
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if([key isEqualToString:@"nickname"]) {
self.name = (NSString *)value;
}
}
NSDictionary *dicTwo = @{@"nickname":@"Mike",@"age":@35};
HFPerson *personTwo = [[HFPerson alloc] init];
[personTwo setValuesForKeysWithDictionary:dicTwo];
NSLog(@"personTwo.name = %@, age = %ld",personTwo.name,personTwo.age);
输出personTwo.name = Mike, age = 35
模型转字典,重写valueForUndefinedKey方法
- (nullable id)valueForUndefinedKey:(NSString *)key {
if([key isEqualToString:@"nickname"]) {
return self.name;
}
return nil;
}
HFPerson *person = [[HFPerson alloc] init];
[person setValue:@"John" forKey:@"name"];
[person setValue:@30 forKey:@"age"];
NSDictionary *tmp = [person dictionaryWithValuesForKeys:@[@"nickname",@"age"]];
NSLog(@"tmp = %@",tmp);
输出结果:
tmp = {
age = 30;
nickname = John;
}
KVO
KVO全称是Key-Value-Observing,又称“键值监听”,可以用于监听某个对象属性值的改变。
KVO和NSNotificationCenter都是iOS观察者模式的一种实现,区别是NSNotificationCenter一对多,KVO是一对一。
常见API
//1、注册KVO监听
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
//2、KVO监听实现
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
//3、移除KVO监听
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
1、注册KVO监听
observer:观察者,监听属性变化的对象,该对象必须实现observeValueForKeyPath:ofObject:change:context方法。
keyPath:被观察的属性名称,要和声明的属性名称一致。
options:回调方法中收到被观察者属性的旧值或新值等,用于指定观察行为。
context:上下文标识符,用于区分不同的观察者,可为空。
2、KVO监听实现
keyPath:被观察的属性名称,要和声明的属性名称一致。
object:被观察的对象change:回调方法中收到被观察者属性的旧值或新值等,用于指定观察行为。
context:上下文标识符,用于区分不同的观察者,可为空。
3、移除KVO监听
observer:要移除的观察者
keyPath:要移除观察的属性名称,要和声明的属性名称一致。
context:上下文标识符,用于区分不同的观察者,可为空
注意:
1、removeObserver需要在观察者消失之前,否则会crash。
2、add和remove成对出现。
KVO原理
KVO的本质是为了改变setter方法的调用,实现原理就是当调用addObserverForKeyPath时,系统利用iisa混写技术(isa-swizzling),在运行时动态创建NSKVONotifying_A,将原来A的isa指向NSKVONotifying_A,NSKVONotifying_A的superClass指向原来的类。重写NSKVONotifying_A的setter方法,通过重写setter方法,达到可以通知所有观察者的目的。
@interface HFPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)printDescription;
@end
#import "HFPerson.h"
#import <objc/runtime.h>
@implementation HFPerson
- (void)printDescription {
NSLog(@"isa = %@, super class = %@", NSStringFromClass(object_getClass(self)), NSStringFromClass(class_getSuperclass(object_getClass(self))));
NSLog(@"self = %@, [self superclass] = %@",self, [self superclass]);
NSLog(@"age setter pointer: %p", class_getMethodImplementation(object_getClass(self), @selector(setAge:)));
NSLog(@"name setter pointer: %p", class_getMethodImplementation(object_getClass(self), @selector(setName:)));
NSLog(@"printDescription pointer: %p", class_getMethodImplementation(object_getClass(self), @selector(printDescription)));
}
@end
@property (nonatomic, strong) HFPerson *person;
self.person = [[HFPerson alloc]init];
self.person.name = @"HaiFei";
//
NSLog(@" - - - 监听前打印 - - - ");
[self.person printDescription];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld context:nil];
self.person.name = @"haifei";
NSLog(@" - - - 监听后打印 - - - ");
[self.person printDescription];
[self.person removeObserver:self forKeyPath:@"name"];
NSLog(@" - - - 移除监听后打印 - - - ");
[self.person printDescription];
// 实现观察者方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"name changed to: %@", change[NSKeyValueChangeNewKey]);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
输出打印
- - - 监听前打印 - - -
isa = HFPerson, super class = NSObject
self = <HFPerson: 0x6000037307a0>, [self superclass] = NSObject
age setter pointer: 0x10ea348b0
name setter pointer: 0x10ea34860
printDescription pointer: 0x10ea34710
name changed to: haifei
- - - 监听后打印 - - -
isa = NSKVONotifying_HFPerson, super class = HFPerson
self = <HFPerson: 0x6000037307a0>, [self superclass] = NSObject
age setter pointer: 0x10ea348b0
name setter pointer: 0x7ff800bdd491
printDescription pointer: 0x10ea34710
- - - 移除监听后打印 - - -
isa = HFPerson, super class = NSObject
self = <HFPerson: 0x6000037307a0>, [self superclass] = NSObject
age setter pointer: 0x10ea348b0
name setter pointer: 0x10ea34860
printDescription pointer: 0x10ea34710
代码分析可知,创建监听后,创建了新的类isa指向NSKVONotifying_HFPerson,并且监听属性的setter方法也发生了改变,从而达到了监听的目的。
在移除监听后isa又指向原来的类,监听属性的setter方法地址也变为原来的地址。
整个过程中未被监听的属性的setter方法地址没有变化,大概可以推断出新的类通过superClass指针获取原类的setter地址。
核心实现是NSKVONotifying_HFPerson内重写setter方法
- (void)setName:(NSString *)name {
//表示属性值即将发生变化 -keyPath和注册kvo时key的一致
[self willChangeValueForKey:@"keyPath"];
//调用父类setter方法,即原类实现
//表示属性值已经发生变化
[self didChangeValueForKey:@"keyPath"];
}
常见问题
1、直接修改成员变量的值,会不会触发KVO
不会。KVO本质是生成找一个子类,重写父类的setter方法,直接修改员变量的值,不会触发setter方法。
但是可以仿写,在赋值前后加上
willChangeValueForKey
赋值
didChangeValueForKey
2、KVC修改属性会触发KVO吗?
会,在使用setValue:ForKey:修改属性的时候,会调用到属性的setter方法,最终触发到监听回调事件。
3、KVO和KVC的keypath一定是属性吗?
KVC支持实例变量,KVO只能手动支持实例变量的KVO监听。
4、KVO与代理的效率对比
KVO的效率比代理低,因为KVO需要动态的生产中间类,比较耗时。
5、KVO是如何监听数组元素变化的?
需要搭配KVC的mutableArrayValueForKey获得被监听属性,然后更新。
self.mutArray = [NSMutableArray arrayWithObject:@"one"];
[self addObserver:self forKeyPath:@"mutArray" options:NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld context:nil];
[[self mutableArrayValueForKey:@"mutArray"]addObject:@"two"];
[self removeObserver:self forKeyPath:@"mutArray"];
// 实现观察者方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"mutArray"]) {
NSLog(@"tmpName changed to: %@", change[NSKeyValueChangeNewKey]);
NSLog(@"self.mutArray = %@",self.mutArray);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
打印输出
tmpName changed to: (
two
)
self.mutArray = (
one,
two
)