iOS kvo kvc
KVC (键值编码)
Objective-C还支持一种更灵活的操作方式,这种方式允许以字符串形式间接操作对象的属性,这种方式的全称是Key Value Coding(简称KVC),即键值编码。
最基本的KVC由NSKeyValueCoding协议提供支持,最基本的操作属性的两个方法如下:
- setValue:属性值forKey:属性名:为指定属性设置值。
- valueForKey:属性名:获取指定属性的值。
用法如下:
@property (nonatomic, copy) NSString *name;
设定:
[object setValue:@"Suxiaoyao" forKey:@"name"]
取值:
NSString *nameStr = [object valueForKey:@"name"]
无论是setValue:属性值forKey:还是valueForKey:属性名:,它们的实现逻辑都是差不多的:
1.首先会从调用方法的类中寻找setter或getter方法,找到就调用,否则下一步
2.然后从调用对象中找_name成员变量,找到赋值,否则下一步
3.从调用对象中找name成员变量,找到赋值,否则下一步
4.调用setValue: forUndefinedKey:
方法或valueForUndefinedKey:
方法,默认会引发一个异常,将会导致程序奔溃 ;
(为了避免奔溃,我们往往要重写setValue: forUndefinedKey:
方法或valueForUndefinedKey:
方法,这样的话,当KVC操作并不存在的key时,KVC机制总是会调用重写的方法进行处理,通过这种处理机制,可以非常方便的定制自己的处理行为。)
处理nil值
当调用KVC来设置对象的属性时,如果属性的类型是对象类型(如NSString),尝试将属性设置为nil,是合法的,程序可以正常运行。
但是如果属性的类型是基本类型(如int、float、double),尝试将属性设置为nil,程序将会崩溃引发以下异常**‘NSInvalidArgumentException’,并且从提示信息可以知道setNilValueForKey:**方法导致了这个异常。
这时可以重写setNilValueForKey ;
Key Path
当我们访问对象属性的属性时,就会用到key路径;
假设 person 对象有属性 address,address 有属性 city,我们可以这样通过 person 来访问 city:
[person valueForKeyPath:@“address.city”];
KVC协议中为操作Key路径的方法如下:
- setValue:forKeyPath: 根据Key路径设置属性值
- valueForKeyPath: 根据Key路径获取属性值
同时key路径对集合操作也支持:
如下:
NSArray *a = @[@4, @84, @2];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.self"]);
我们有一个 Transaction 对象的数组,对象有属性 amount 的话,我们可以这样获得最大的 amount:
NSArray *a = @[transaction1, transaction2, transaction3];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.amount"]);
KVC的优点
使用KVC编程的优势是更加简洁,更适合提炼一些通用性质的代码。由于KVC允许通过字符串形式来操作对象的属性,这个字符串既可是常量,也可是变量,因此具有极高的灵活性。
KVO (键值监听)
如果程序存在的需求是:在数据模型组件的状态数据发生改变时,视图组件能动态地更新自己,及时显示数据模型组件更新后的数据。
KVO机制NSKeyValueObserving协议提供支持,当然,NSObject遵守了该协议,因此,NSObject的子类都可使用该协议中的方法,该协议包含如下常用的方法可用于注册监听器:
-
addObserver:forKeyPath:options:context: 注册一个监听器用于监听指定Key路径
-
removeObserver:forKeyPath: 为指定Key路径删除指定的监听器
-
removeObserver:forKeyPath:context: 为指定Key路径删除指定的监听器,只是多了一个context参数。
KVO编程的步骤如下:
- 为被监听对象(通常是数据模型组件)注册监听器
- 重写监听器的observeValueForKeyPath:ofObject:change:context:方法
- 移除监听器
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person = [[person alloc] init] ;
[self.person setValue:@"hello" forKey:@"name"] ;
self.view.backgroundColor = UIColor.whiteColor ;
UIButton* btn = [UIButton buttonWithType:UIButtonTypeCustom] ;
btn.frame = CGRectMake(30, 30, 60, 60) ;
btn.backgroundColor = UIColor.redColor ;
[btn addTarget:self action:@selector(pressbtn) forControlEvents:UIControlEventTouchUpInside] ;
[self.view addSubview:btn] ;
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil] ;
}
- (void)pressbtn {
[self.person setValue:@"hhh" forKey:@"name"] ;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"name has change") ;
NSLog(@"%@",self.person.name) ;
}
}
[self.person addObserver:self forKeyPath:@“name” options:NSKeyValueObservingOptionNew context:nil] ;
这里的self.person是被观察者self为观察者,观察者监听被观察者对应keypath的成员变量,变化就通知观察者调用其- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { ;
KVO的优点
KVO这种编码方式使用起来很简单,很适用于model修改后,引发的view的变化这种情况,就像上边的例子那样,当更改属性的值后,监听对象会立即得到通知。
KVO的实现原理
KVO的全称是Key-Value Observing,俗称"键值监听",可以用于监听摸个对象属性值得改变。
首先从一个问题入手,如果不通过setter方法修改被监听的属性值,KVO监听还会生效吗 ?
实际上这么问了,肯定不会生效了;具体原因在于它的实现原理;
打印两个实例对象person1和person2的isa指针指向的类对象,假设person1添加了KVO监听,person2没有 ;
这个时候会发现person1的isa指针打印出的是: NSKVONotifying_DLPerson而person2的isa指针打印出来的是: DLPerson
所以说,如果对象没有添加KVO监听那么它的isa指向的就是自己原来的类对象,如下图
当一个对象添加了KVO的监听时,当前对象的isa指针指向的就不是你原来的类,指向的是另外一个类对象,如下图
- NSKVONotifying_DLPerson类是 Runtime动态创建的一个类,在程序运行的过程中产生的一个新的类。
- NSKVONotifying_DLPerson类是DLPerson的一个子类。
- NSKVONotifying_DLPerson类存在自己的 setAge:、class、dealloc、isKVOA…方法。
在明确NSKVONotifying_DLPerson类是DLPerson的一个子类的情况下,当修改值的时候,大概会执行下面伪代码中的setter方法:
///> NSKVONotifying_DLPerson.m 文件
#import "NSKVONotifying_DLPerson.h"
@implementation NSKVONotifying_DLPerson
- (void)setAge:(int)age{
_NSSetIntValueAndNotify(); ///> 文章末尾 知识点补充小结有此方法来源
}
void _NSSetIntValueAndNotify(){
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
///> 通知监听器 key发生了改变
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
KVO调用顺序
1.修改值的时候,调用NSKVONotifying_DLPerson类的setter方法 ;
2.在这个setter方法中会按顺序执行下面三个方法
- 调用willChangeValueForKey:
- 调用原来的setter实现
- 调用didChangeValueForKey:(这里执行监听通知的回调)
KVC的实现原理
setValue:forKey:的原理
调用setValue:forKey:时
-
首先会查找setKey:、_setKey: (按顺序查找),有直接调用,没有,先查看accessInstanceVariablesDirectly方法
+ (BOOL)accessInstanceVariablesDirectly{ return YES; ///> 可以直接访问成员变量 // return NO; ///> 不可以直接访问成员变量, ///> 直接访问会报NSUnkonwKeyException错误 }
-
如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量,找到直接复制,未找到报错NSUnkonwKeyException错误
valueForKey:的原理
额,基本同上
一些面试题补充
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
如何手动触发KVO?
手动调用willChangeValueForKey:和didChangeValueForKey:
直接修改成员变量会触发KVO么?
不会触发KVO,因为直接修改成员变量并没有走set方法。
通过KVC修改属性会触发KVO么?
看情况,看有没有涉及setter方法的调用
KVC的赋值和取值过程是怎样的?原理是什么?
看上面的流程图