键值编码KVC与键值监听KVO
键值编码KVC
设置和获取对象的属性值有三种:setter/getter方法、点语法、KVC。KVC即Key Value Coding,以字符串形式间接操作对象的属性。
最基本的KVC由NSKeyValueCoding协议提供支持,最基本的操作属性的两个方法如下:
setValue:属性值 forKey:属性名
:为指定属性设置值。valueForKey:属性名
:获取指定属性的值。
#import <Foundation/Foundation.h>
@interface FKUser : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, copy) NSString* pass;
@property (nonatomic, copy) NSDate* birth;
@end
@implementation FKUser
@end
int main(int argc, char* argv[]){
@autoreleasepool{
FKUser* user = [[FKUser alloc] init];
//使用KVC方式为name、pass、birth属性设置值
[user setValue:@"wukong.sun" forKey:@"name"];
[user setValue:@"1455" forKey:@"pass"];
[user setValue:[[NSDate alloc] init] forKey:@"birth"];
//使用KVC方式获取FKUser对象的属性值
NSLog(@"user's name is %@", [user valueForKey:@"name"]);
NSLog(@"user's pass is %@", [user valueForKey:@"pass"]);
NSLog(@"user's date is %@", [user valueForKey:@"birth"]);
}
}
编译执行以上程序,输出内容如下:
user's name is wukong.sun
user's pass is 1455
user's date is 2022-07-28 12:35:09 +0000
KVC的底层执行机制
以对上述程序中FKUser类对象的name属性采取KVC方式操作为例:
setValue:属性值 forKey:@“name” | valueForKey:@“name” | |
---|---|---|
① | 优先考虑调用setName:属性值; 代码通过setter方法完成设置 | 优先考虑调用name; 代码通过getter方法获取属性值 |
② | 若没有setName: 方法,KVC对_name 成员变量赋值 | 若没有name 方法,KVC返回_name 成员变量的值 |
③ | 既没有setName: 方法,又没有_name 成员变量,KVC对name 成员变量赋值 | 既没有name 方法,又没有_name 成员变量,KVC返回name 成员变量的值 |
④ | 上三步都没有找到,系统会执行对象的setValue: forUndefinedKey: 方法 | 上三步都没有找到,系统会执行对象的valueForUndefinedKey: 方法 |
默认的
setValue: forUndefinedKey:
方法和valueForUndefinedKey:
方法是引发一个异常,这个异常会使程序因异常而结束。
#import <Foundation/Foundation.h>
@interface FKDog : NSObject{
@package
NSString* name;
NSString* _name;
}
@end
@implementation FKDog{
int age;
}
@end
int main(int argc, char* argv[]){
@autoreleasepool{
FKDog* dog = [[FKDog alloc] init];
[dog setValue:@"旺财" forKey:@"name"];
NSLog(@"dog->name:%@", dog->name);
NSLog(@"dog->_name:%@", dog->_name);
[dog setValue:[NSNumber numberWithInt:5] forKey:@"age"];
NSLog(@"dog's age is %@", [dog valueForKey:@"age"]);
}
}
以上程序输出:
dog->name:(null)
dog->_name:旺财
dog's age is 5
处理不存在的key
以KVC方式操作属性时,属性可能并不存在,即既没有对应的setter/getter方法。也没有对应的成员变量,这是KVC会自动调用setValue: forUndefinedKey:
方法和valueForUndefinedKey:
方法,引发异常而使程序结束。但实际业务中可能并不想程序因此结束,为了避免这种情况,可以重写这两个方法。
只需在类的实现部分重写这两个方法,不需要在类接口部分声明它们。
处理nil值
以KVC方式操作属性时,如果属性的类型是基本数据类型(如int、float、double),且程序传入了对应的值,那么KVC可以正确地进行设置。但如果尝试为基本类型的属性设置一个nil,程序会引发NSInvalidArgumentException
异常而出现错误。当程序尝试为某个属性设置nil值,而该属性并不支持接受nil值时,程序就会自动执行该对象的setNilValurForKey:
方法,这个方法默认会触发NSInvalidArgumentException
异常。为了避免这种情况,可以重写setNilValurForKey:
方法。
key路径
KVC除了可以操作对象的属性,还可操作对象的“复合属性”。比如A对象内包含一个B类型的b对象,B对象有b1、b2两个属性,那么KVC可以通过b.b1
、b.b2
这种key路径来操作A对象的b属性的b1、b2属性。
KVC协议中操作key路径的方法如下:
setValue:forKeyPath:
:根据key路径设置属性值。valueForKeyPath:
:根据key路径获取属性值。
键值监听KVO
iOS应用通常把应用程序组件分为数据模型组件和视图组件,其中数据模型组件负责维护应用程序的状态数据,而视图组件负责显示数据模型组件内部的数据。
考虑以下需求:在数据模型组件的数据发生改变时,视图组件能动态地更新自己,及时显示数据模型组件的最新数据。
这时候,KVO就派上了用场。
KVO机制由NSKeyValueObserving
协议提供支持,由于NSObject类遵守该协议,因此所有的Objective-C类都可使用该协议中的方法。该协议包含了如下常用方法用于注册监听器。
addObserver:forKeyPath:options:context:
:注册一个监听器用于监听指定的key路径。removeObserver:forKeyPath:
:为key路径删除指定的监听器。removeObserver:forKeyPath:context:
:为key路径删除指定的监听器。只是多了一个context参数。
对于上面的需求,很容易想到如下解决方案:让视图组件监听数据模型组件的改变,当数据模型组件的key路径对应的属性值发生改变时,作为监听器的视图组件将被触发,触发时就会回调监听器自身的监听方法。
#import <Foundation/Foundation.h>
@interface FKModel : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) int price;
@end
@implementation FKModel
@end
@interface FKView : NSObject
@property (nonatomic, weak) FKModel* model;
- (void) showModelInfo;
@end
@implementation FKView
- (void) showModelInfo{
NSLog(@"name is %@, price is %d", self.model.name, self.model.price);
}
//自定义setModel:方法
- (void) setModel:(FKModel*) model{
self->_model = model;
//为model添加监听器,监听model的name属性的改变
[self.model addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
//为model添加监听器,监听model的price属性的改变
[self.model addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew context:nil];
}
//重写该方法,当被监听的数据模型组件发生改变时,就会回调监听器的该方法
- (void) observeValueForKeyPath:(NSString*) keyPath ofObject:(id) object change:(NSDictionary*) change context:(void*) context{
NSLog(@"--observeValueForKeyPath方法被调用--");
NSLog(@"被修改的keyPath为:%@", keyPath);
NSLog(@"被修改的对象为:%@", object);
NSLog(@"被修改的属性值为:%@", [change objectForKey:@"new"]);
NSLog(@"被修改的上下文为:%@", context);
}
- (void) dealloc{
//删除监听器
[self.model removeObserver:self forKeyPath:@"name"];
[self.model removeObserver:self forKeyPath:@"price"];
}
@end
int main(int argc, char* argv[]){
@autoreleasepool{
FKModel* model = [[FKModel alloc] init];
model.name = @"疯狂iOS讲义";
model.price = 99;
FKView* view = [[FKView alloc] init];
view.model = model;
[view showModelInfo];
//更改model对象的属性,将会触发监听器的方法
model.name = @"疯狂XML讲义";
model.price = 66;
}
}
编译执行以上程序,输出如下:
name is 疯狂iOS讲义, price is 99
--observeValueForKeyPath方法被调用--
被修改的keyPath为:name
被修改的对象为:<FKModel: 0x13de11dd0>
被修改的属性值为:疯狂XML讲义
被修改的上下文为:(null)
--observeValueForKeyPath方法被调用--
被修改的keyPath为:price
被修改的对象为:<FKModel: 0x13de11dd0>
被修改的属性值为:66
被修改的上下文为:(null)
这样就实现了前面的需求。