KVC /KVO的底层原理和使用场景
-
KVC(key value coding)
(一)原理
(1)赋值时首先判断有没有对应的set方法,如果有直接赋值
(2)如果没有set方法,查看有没有和key一样的成员变量,如果有,直接赋值
(3)如果没有成员变量,查找有没有对应的属性,如果有,直接赋值
(4)如果都没有,调用setvalue forUndefinedKey方法
(二)使用场景
(1)赋值- (void)setValue:(nullable id)value forKey:(NSString *)key; - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
(2)取值
- (id)valueForKey:(NSString *)key; - (id)valueForKeyPath:(NSString *)keyPath; - (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
(3)字典转模型
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id>
(4)模型转字典
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
KVO
(一)实现原理
(1)对象添加observer后,runtime会自动生成一个中间类,让对象的isa指针指向这个中间类(继承自原类),
(2)中间类会重写set方法,并在调用元set方法的前后添加willChangeValue和didChangeValue方法,继而通知observer
如果需要手动触发kvo,只需调用willChangeValue和didChangeValue方法即可
-
1 KVC(KeyValueCoding)
1.1 KVC 常用的方法
(1)赋值类方法 - (void)setValue:(nullable id)value forKey:(NSString *)key; - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues; (2)取值类方法 // 能取得私有成员变量的值 - (id)valueForKey:(NSString *)key; - (id)valueForKeyPath:(NSString *)keyPath; - (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
1.2 KVC 底层实现原理
当一个对象调用setValue:forKey: 方法时,方法内部会做以下操作: 1.判断有没有指定key的set方法,如果有set方法,就会调用set方法,给该属性赋值 2.如果没有set方法,判断有没有跟key值相同且带有下划线的成员属性(_key).如果有,直接给该成员属性进行赋值 3.如果没有成员属性_key,判断有没有跟key相同名称的属性.如果有,直接给该属性进行赋值 4.如果都没有,就会调用 valueforUndefinedKey 和setValue:forUndefinedKey:方法
1.3 KVC 的使用场景
1.3.1 赋值
(1) KVC 简单属性赋值
Person *p = [[Person alloc] init]; // p.name = @"jack"; // p.money = 22.2; 使用setValue: forKey:方法能够给属性赋值,等价于直接给属性赋值 [p setValue:@"rose" forKey:@"name"]; [p setValue:@"22.2" forKey:@"money"];
(2) KVC复杂属性赋值
//给Person添加 Dog属性 Person *p = [[Person alloc] init]; p.dog = [[Dog alloc] init]; // p.dog.name = @"阿黄"; 1)setValue: forKeyPath: 方法的使用 //修改p.dog 的name 属性 [p.dog setValue:@"wangcai" forKeyPath:@"name"]; [p setValue:@"阿花" forKeyPath:@"dog.name"]; 2)setValue: forKey: 错误用法 [p setValue:@"阿花" forKey:@"dog.name"]; NSLog(@"%@", p.dog.name); 3)直接修改私有成员变量 [p setValue:@"旺财" forKeyPath:@"_name"];
(3) 添加私有成员变量
Person 类中添加私有成员变量_age [p setValue:@"22" forKeyPath:@"_age"];
1.3.2 字典转模型
(1)简单的字典转模型 +(instancetype)videoWithDict:(NSDictionary *)dict { JLVideo *videItem = [[JLVideo alloc] init]; //以前 // videItem.name = dict[@"name"]; // videItem.money = [dict[@"money"] doubleValue] ; //KVC,使用setValuesForKeysWithDictionary:方法,该方法默认根据字典中每个键值对,调用setValue:forKey方法 // 缺点:字典中的键值对必须与模型中的键值对完全对应,否则程序会崩溃 [videItem setValuesForKeysWithDictionary:dict]; return videItem; } (2)复杂的字典转模型 注意:复杂字典转模型不能直接通过KVC 赋值,KVC只能在简单字典中使用,比如: NSDictionary *dict = @{ @"name" : @"jack", @"money": @"22.2", @"dog" : @{ @"name" : @"wangcai", @"money": @"11.1", } }; JLPerson *p = [[JLPerson alloc]init]; // p是一个模型对象 [p setValuesForKeysWithDictionary:dict]; 内部转换原理: // [p setValue:@"jack" forKey:@"name"]; // [p setValue:@"22.2" forKey:@"money"]; // [p setValue:@{ // @"name" : @"wangcai", // @"money": @"11.1", // // } forKey:@"dog"]; //给 dog赋值一个字典肯定是不对的 (3)KVC解析复杂字典的正确步骤 NSDictionary *dict = @{ @"name" : @"jack", @"money": @"22.2", @"dog" : @{ @"name" : @"wangcai", @"price": @"11.1", }, //人有好多书 @"books" : @[ @{ @"name" : @"5分钟突破iOS开发", @"price" : @"19.8" }, @{ @"name" : @"3分钟突破iOS开发", @"price" : @"24.8" }, @{ @"name" : @"1分钟突破iOS开发", @"price" : @"29.8" } ] }; XMGPerson *p = [[XMGPerson alloc] init]; p.dog = [[XMGDog alloc] init]; [p.dog setValuesForKeysWithDictionary:dict[@"dog"]]; //保存模型的可变数组 NSMutableArray *arrayM = [NSMutableArray array]; for (NSDictionary *dict in dict[@"books"]) { //创建模型 Book *book = [[Book alloc] init]; //KVC [book setValuesForKeysWithDictionary:dict]; //将模型保存 [arrayM addObject:book]; } p.books = arrayM; 备注: (1)当字典中的键值对很复杂,不适合用KVC; (2)服务器返还的数据,你可能不会全用上,如果在模型一个一个写属性非常麻烦,所以不建议使用KVC字典转模型
1.3.3 取值
(1) 模型转字典
Person *p = [[Person alloc]init]; p.name = @"jack"; p.money = 11.1; //KVC取值 NSLog(@"%@ %@", [p valueForKey:@"name"], [p valueForKey:@"money"]); //模型转字典, 根据数组中的键获取到值,然后放到字典中 NSDictionary *dict = [p dictionaryWithValuesForKeys:@[@"name", @"money"]]; NSLog(@"%@", dict);
(2) 访问数组中元素的属性值
Book *book1 = [[Book alloc] init]; book1.name = @"5分钟突破iOS开发"; book1.price = 10.7; Book *book2 = [[Book alloc] init]; book2.name = @"4分钟突破iOS开发"; book2.price = 109.7; Book *book3 = [[Book alloc] init]; book3.name = @"1分钟突破iOS开发"; book3.price = 1580.7; // 如果valueForKeyPath:方法的调用者是数组,那么就是去访问数组元素的属性值 // 取得books数组中所有Book对象的name属性值,放在一个新的数组中返回 NSArray *books = @[book1, book2, book3]; NSArray *names = [books valueForKeyPath:@"name"]; NSLog(@"%@", names); //访问属性数组中元素的属性值 Person *p = [[Person alloc]init]; p.books = @[book1, book2, book3]; NSArray *names = [p valueForKeyPath:@"books.name"]; NSLog(@"%@", names);
2 KVO (Key Value Observing)
2.1 KVO 的底层实现原理
(1)KVO 是基于 runtime 机制实现的 (2)当一个对象(假设是person对象,对应的类为 JLperson)的属性值age发生改变时,系统会自动生成一个继承自JLperson的类NSKVONotifying_JLPerson,在这个类的 setAge 方法里面调用 [super setAge:age]; [self willChangeValueForKey:@"age"]; [self didChangeValueForKey:@"age"]; 三个方法,而后面两个方法内部会主动调用 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context方法,在该方法中可以拿到属性改变前后的值.
2.2 KVO的作用
- 作用:能够监听某个对象属性值的改变
// 利用KVO监听p对象name 属性值的改变 Person *p = [[XMGPerson alloc] init]; p.name = @"jack"; /* 对象p添加一个观察者(监听器) Observer:观察者(监听器) KeyPath:属性名(需要监听哪个属性) */ [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"]; /** * 利用KVO 监听到对象属性值改变后,就会调用这个方法 * * @param keyPath 哪一个属性被改了 * @param object 哪一个对象的属性被改了 * @param change 改成什么样了 */ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context { // NSKeyValueChangeNewKey == @"new" NSString *new = change[NSKeyValueChangeNewKey]; // NSKeyValueChangeOldKey == @"old" NSString *old = change[NSKeyValueChangeOldKey]; NSLog(@"%@-%@",new,old); }