键值编码(KVC)与键值监听(KVO)
KVC(Key Value Coding)允许以字符串的形式间接操作对象的属性。
简单的KVC
最基本的KVC由NSKeyValueCoding协议提供支持,最基本的操作属性的两个方法如下
- setVaule:属性值 forKey:为制定属性设置值
- valueForKey:属性名:获取指定属性的值
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKUser : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *pass;
@property (nonatomic, copy) NSDate *birth;
@end
NS_ASSUME_NONNULL_END
//
#import <Foundation/Foundation.h>
#import "FKUser.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
FKUser *user = [[FKUser alloc] init];
[user setValue:@"花城" forKey:@"name"];
[user setValue:@"1155" forKey:@"pass"];
[user setValue:[[NSDate alloc] init] forKey:@"birth"];
NSLog(@"user的name为%@", [user valueForKey:@"name"]);
NSLog(@"user的pass为%@", [user valueForKey:@"pass"]);
NSLog(@"user的birth为%@", [user valueForKey:@"birth"]);
}
return 0;
}
上面的代码先创建一个user对象,在对对象的属性通过KVC方式设置属性值,最后通过KVC方式获取指定属性的值
在KVC编程方式中,无论调用setValue:forKey:方法还是调用valueForKey:方法,都是通过NSString对象来指定被操作属性的,其中forKey标签用于传入属性名
setValue:属性值forKey@“name”代码
- 程序优先考虑调用“setName:属性值”;代码通过setter方法完成设置
- 如果该类没有setName:方法,那么KVC机制回搜索该类中名为_name的成员变量,无论该成员变量是在类接口部分定义还是在类实现部分定义,也无论用哪个访问权限控制符修饰,这条KVC代码底层实际上就是对_name成员变量赋值
- 如果该类即没有setName:方法,也没有定义_name成员变量,那么KVC机制回搜索该类中名为name的成员变量,无论该成员变量是在类接口部分定义还是在类实现部分定义,也无论用哪个访问控制符修饰,这条KVC代码底层实际上就是对name成员变量赋值。
- 如果上边三步都没有找到,那么系统会执行该对象的setValue:forUndefinrdKey:方法
默认的valueForUndefinedKey:方法实现就是引发一个异常,这个异常将会导致程序因为异常而结束。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKDog : NSObject {
@package
NSString *name;
NSString *_name;
}
@end
NS_ASSUME_NONNULL_END
//
#import "FKDog.h"
@implementation FKDog {
int age;
}
@end
FKDog *dog = [[FKDog alloc] init];
[dog setValue:@"戚容" forKey:@"name"];
NSLog(@"dog->name = %@", dog->name);
NSLog(@"dog->_name = %@", dog->_name);
[dog setValue:[NSNumber numberWithInt:800] forKey:@"age"];
NSLog(@"dog的age%@", [dog valueForKey:@"age"]);
我们先对dog的name属性进行赋值,但是由于FKDog类并不存在setName:方法,因此我们对dog的成员变量_name进行赋值。我们输出dog->name时,可以看到输出“戚容”。所以我们输出dog->name时为空,而输出dog->_name时不为空。
最后我们通过KVC对dog的age属性赋值并访问
处理不存在的key
当我们使用KVC方式操作属性时,这些属性可能并不存在,即不存在对应的setter,getter方法,也不存在对应的成员变量,KVC会自动调用setValue:forUndefinedKey:和valueForUndefinedKey:方法,但是系统默认实现的这两个方法仅仅是引发异常,并没有进行任何特别处理
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKApple : NSObject
@end
NS_ASSUME_NONNULL_END
//
#import "FKApple.h"
@implementation FKApple
@end
FKApple *apple = [[FKApple alloc] init];
[apple setValue:@"monster" forKey:@"name"];
[apple valueForKey:@"name"];
提示说程序尝试设定的name对象不存在,因此引发异常。
这句提示就是setValue:forUndefinedKey方法的默认实现,这个实现引发一个NSUnknownKeyExpection异常,并结束程序,这个默认方法的实现可能并不适合实际,可能我们不想让他结束。这时候就要重写这个方法。
//
#import "FKApple.h"
@implementation FKApple
- (void)setValue:(id)value forUndefinedKey:(nonnull NSString *)key{
NSLog(@"这个key不存在,你是不是敲错了?");
NSLog(@"你尝试设定的value为:%@", value);
}
@end
显而易见,我们通过KVC设置对象的属性时没有发生任何异常,那为什么还是崩了呢?
因为还有这一句代码
[apple valueForKey:@“name”];
我们尝试通过KVC访问对象的name key,因为我们不存在name方法,也不存在name,_name成员变量,因此KVC会调用valueForUndefinedKey:方法,这时我们重写这个方法。
#import "FKApple.h"
@implementation FKApple
- (void)setValue:(id)value forUndefinedKey:(nonnull NSString *)key{
NSLog(@"这个key不存在,你是不是敲错了?");
NSLog(@"你尝试设定的value为:%@", value);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"这个key不存在,你是不是敲错了?");
return key;
}
@end
可以发现重写之后就没有异常了。
当KVC操作不存在的key时,KVC机制总是调用重写过的方法进行处理,通过这种处理机制,可以非常方便的定制自己的处理行为。
处理nil值
当通过KVC设置对象的属性时,如果属性是基本类型(int, float,doule),并且我们给他一个对应的属性值,那么程序可以正确的进行设置,但是如果我们为基本类型的属性设置一个nil,会发生什么事情呢,KVC会把这个东西当作0还是1?
// Created by 王璐 on 2023/5/13.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FKItem : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int price;
@end
NS_ASSUME_NONNULL_END
FKItem *item = [[FKItem alloc] init];
[item setValue:nil forKey:@"name"];
[item setValue:nil forKey:@"price"];
NSLog(@"item的name为%@", [item valueForKey:@"name"]);
NSLog(@"item的price为%@", [item valueForKey:@"price"]);
我们设置了两个属性,一个是NSString类型,它可以接受nil,另一个是int类型,不能接受nil。
提示我们引发了一个NSInvidalArguementExpection异常。这是因为int类型的值不能接受nil造成的
这段提示信息是由程序中的setNilValueForKey:方法所产生的。也就是说,当程序尝试为某个属性值设置nil值时,如果该属性并不接受nil值,那么程序会自动执行该属性的setNilValueForKey:方法。我们重写一下这个方法。
//
#import "FKItem.h"
@implementation FKItem
- (void)setNilValueForKey:(NSString *)key{
if ([key isEqualToString:@"price"]) {
_price = 0;
} else {
[super setNilValueForKey:key];
}
}
@end
如果我们尝试将key为price的属性设置为nil,那么程序将会直接将_price成员变量设置为0,说明FKItem的price属性会把nil当成0处理,设置其他属性则不做处理。
key路径
KVC除了可操作对象的属性外,还可以操作对象的复合属性。所谓复合属性,KVC将其称为key路径。比如KFOrder对象内包含一个FKItem的类型的属性item,FKitem又包含了name属性和price属性,那么KVC就可以通过item.name,item.price这种key路径来操作FKOrder对象中的item属性和price属性
在KVC中操作key路径的方法如下。
- setValue:forKeyPath:根据key路径设置属性值
- valueForKeyPath:根据key路径获取属性值
#import <Foundation/Foundation.h>
#import "FKItem.h"
NS_ASSUME_NONNULL_BEGIN
@interface FKOrder : NSObject
@property (nonatomic, strong) FKItem *item;
@property (nonatomic, assign) int amount;
- (int)totalPrice;
@end
NS_ASSUME_NONNULL_END
FKOrder *order = [[FKOrder alloc] init];
[order setValue:@"5000" forKey:@"amount"];
[order setValue:[[FKItem alloc] init] forKey:@"item"];
[order setValue:@"书包" forKeyPath:@"item.name"];
[order setValue:[NSNumber numberWithInt:5] forKeyPath:@"item.price"];
NSLog(@"订单包含%@个%@, 总价为%@",[order valueForKey:@"amount"], [order valueForKeyPath:@"item.name"], [order valueForKey:@"totalPrice"]);
setValue:forKey:,valueForKey:设置复合属性并获取复合属性的值,这样就可以直接操作FKOreder对象中的item属性的name属性了,也可以直接操作FKOreder对象中item属性的price属性
OC的集合同样支持使用KVC进行整体操作
为什么要用KVC来操作呢?使用setter,getter不行吗?是不是这样效率比较高呢?实际上,通过KVC操作的性能比setter,getter更差,使用KVC的优势在于更灵活,更适合提炼一些通用性质的代码。因为KVC的方式允许通过字符串的方式来操作对象的属性,这个字符串既可以是常量,也可以是变量。因此具有极高的灵活性。
KVO(键值坚听)
在iOS应用开发过程中,iOS应用通常会把应用程序组件分开成为数据模型组件和视图组件,其中数据模型组件负责维护应用程序的状态数据,视图组件负责显示数据模型组件内部的状态数据。
我们的需求是:在数据模型组件的状态数据发生改变时,视图组件能动态的更新自己,及时显示数据模型组件更新后的数据。
//
#import <Foundation/Foundation.h>
#import "FKItem.h"
NS_ASSUME_NONNULL_BEGIN
@interface FKItemView : NSObject
@property (nonatomic, weak) FKItem *item;
- (void)showItemInfo;
@end
NS_ASSUME_NONNULL_END
为了能让FKItemView监听得到FKItem组件的状态改变,我们需要为FKItem组件添加一个监控属性改变的监听器。并重写监听器的observerValueForKeyPath:change:context:方法
//
#import <Foundation/Foundation.h>
#import "FKItem.h"
NS_ASSUME_NONNULL_BEGIN
@interface FKItemView : NSObject
@property (nonatomic, weak) FKItem *item;
- (void)showItemInfo;
@end
NS_ASSUME_NONNULL_END
//
#import "FKItemView.h"
@implementation FKItemView
- (void)showItemInfo {
NSLog(@"物品名称为%@, 物品价格为%d", self.item.name, self.item.price);
}
- (void)setItem:(FKItem *)item {
self->_item = item;
[self.item addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
[self.item addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"bserveValueForKeyPath");
NSLog(@"keyPath:%@", keyPath);
NSLog(@"object:%@", object);
NSLog(@"change objectForKey:%@", [change objectForKey:@"new"]);
NSLog(@"change:%@", context);
}
- (void)dealloc {
[self.item removeObserver:self forKeyPath:@"name"];
[self.item removeObserver:self forKeyPath:@"price"];
}
@end
FKItem *item = [[FKItem alloc] init];
item.name = @"花城";
item.price = 800;
FKItemView *view = [[FKItemView alloc] init];
view.item = item;
[view showItemInfo];
item.name = @"huacheng";
item.price = 10000;
我们可以看到监听器监听方法的输出,可以看出监听器完全可以得到我们修改的属性,接下来我们把这些修改的属性加到UI上就好了。