KVC
KVC(key-value coding),键值编码。是指可以在ios开发过程中,可以允许开发者通过key名直接访问对象属性,或给对象属性赋值。这样就可以在runtime中动态访问和修改对象的属性值,而不是在编译时确定,这很重要。
常用的四个方法如下:
- (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
是通过属性名对应的get和set方法进行存取值的,也就是说即使name属性不存在,但是存在getname和setname方法,也可以使用。
key具体使用如下:
[arr setValue:@"newName" forKey:@"arrName"];
NSString* name = [arr valueForKey:@"arrName"];
keypath具体使用如下:
NSString* country1 = people1.address.country;
NSString * country2 = [people1 valueForKeyPath:@"address.country"];
[people1 setValue:@"USA" forKeyPath:@"address.country"];
不允许将值类型设定为nil,可以重写此方法打印错误防止崩溃
-(void)setNilValueForKey:(NSString *)key{
NSLog(@"不能将%@设成nil",key);
}
注意使用set设定时要手动设定为NSNumber这样的对象类型,取出时自动为NSNumber和NSValue这样的对象类型,需要自己转成想要的类型,此处和字典的操作类似。
与字典配合使用
Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";
NSArray* arr = @[@"country",@"province",@"city",@"district"];
//把对象中所有属性按照列表取出放入字典
NSDictionary* dict = [add dictionaryWithValuesForKeys:arr];
NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
//把字典中内容按照Key以及对应属性存入对象
[add setValuesForKeysWithDictionary:modifyDict];
验证KVC正确性
@implementation Address
-(BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{ //在implementation里面加这个方法,它会验证是否设了非法的value
NSString* country = *value;
country = country.capitalizedString;
if ([country isEqualToString:@"Japan"]) {
return NO; //如果国家是日本,就返回NO,这里省略了错误提示,
}
return YES;
}
@end
NSError* error;
id value = @"japan";
NSString* key = @"country";
BOOL result = [add validateValue:&value forKey:key error:&error]; //如果没有重写-(BOOL)-validate:error:,默认返回Yes
if (result) {
NSLog(@"键值匹配");
[add setValue:value forKey:key];
}
else{
NSLog(@"键值不匹配"); //不能设为日本,基他国家都行
}
NSString* country = [add valueForKey:@"country"];
NSLog(@"country:%@",country);
//打印结果
2016-04-20 14:55:12.055 KVCDemo[867:58871] 键值不匹配
2016-04-20 14:55:12.056 KVCDemo[867:58871] country:China
需要手动去调用验证,kvc不会自动调用。
KVC的使用
- 动态取值和设定值
最基本用法 - 访问和修改私有变量
私有变量不开放,不能用普通方法修改,可以用此方法修改。 - model和字典转换
model和字典转换的强大功能基础就是kvc - 修改一些控件的内部属性
必不可少的重要技巧。苹果不开放很多控件的api,可以用kvc访问和修改这些控件属性,例如修改TextField中PlaceHolder的颜色和字体。 - 操作集合
- 实现高阶消息传递
- KVO
KVO的使用
KVO(key-value observing),键值监听。允许对象监测另一个对象的属性,并在改变时接收到事件。一般继承自NSObject的对象都默认支持KVO。
KVO和NSNotificationCenter都是观察者模式的一种实现方式,前者一对一,后者可以一对多。相比于通知,KVO无入侵性,对被观察者部分的代码不需要任何改动。
基本使用
基本方法:
/*
注册监听器
监听器对象为observer,被监听对象为消息的发送者即方法的调用者在回调函数中会被回传
监听的属性路径为keyPath支持点语法的嵌套
监听类型为options支持按位或来监听多个事件类型
监听上下文context主要用于在多个监听器对象监听相同keyPath时进行区分
添加监听器只会保留监听器对象的地址,不会增加引用,也不会在对象释放后置空,因此需要自己持有监听对象的强引用,该参数也会在回调函数中回传
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
/*
删除监听器
监听器对象为observer,被监听对象为消息的发送者即方法的调用者,应与addObserver方法匹配
监听的属性路径为keyPath,应与addObserver方法的keyPath匹配
监听上下文context,应与addObserver方法的context匹配
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
/*
与上一个方法相同,只是少了context参数
推荐使用上一个方法,该方法由于没有传递context可能会产生异常结果
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
/*
监听器对象的监听回调方法
keyPath即为监听的属性路径
object为被监听的对象
change保存被监听的值产生的变化
context为监听上下文,由add方法回传
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
简单例子:
#import <Foundation/Foundation.h>
@interface Account: NSObject
@property (nonatomic, copy) NSString *accountNumber;
@property (nonatomic, assign) double balance;
@end
@implementation Account
//啰嗦一句,这里llvm会自动生成,写不写无所谓
@synthesize accountNumber = _accountNumber;
@synthesize balance = _balance;
@end
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, strong) Account *account;
- (void)setObserver;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
//添加监听器
- (void)setObserver
{
/*
监听器对象为Person类的对象本身,被监听的对象为Person类对象持有的account
监听的属性路径为account的balance,可以监听嵌套的对象比如account有一个对象是bank可以监听bank是否营业,可以写"bank.isOpen"
监听上下文设置为nil,相信很多人在使用的时候都会这么写
*/
[self.account addObserver:self forKeyPath:@"balance" options:NSKeyValueObservingOptionNew context:nil];
/*另一种写法
[self addObserver:self forKeyPath:@"account.balance" options:NSKeyValueObservingOptionNew context:nil];
*/
}
//监听器回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
//判断被监听对象是否为account,并且通过NSString来判断监听属性路径是否一致
if (object == self.account && [keyPath isEqualToString:@"balance"])
{
NSLog(@"NewBalance: %lf", self.account.balance);
}
}
//Person销毁时调用的方法
- (void)dealloc
{
/*
切记,当我们添加监听器时一定要在对象被销毁前删除该监听器
删除监听器传递的参数要与添加监听器传参一致
监听器也不可以重复删除,如果没有注册监听器而去执行删除操作也会抛出异常
*/
[self.account removeObserver:self forKeyPath:@"balance" context:nil];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
p.account = [[Account alloc] init];
p.account.balance = 100.0;
//添加监听器
[p setObserver];
//重新对account的balance赋值后会触发回调函数
//输出: NewBalance: 200.0
p.account.balance = 200.0;
}
return 0;
}
其中addObserver和removeObserver必须成对出现,否则会出现野指针错误。一般推荐在init时addObserver,在dealloc中removeObserver。
context:
当多个监视器同时监测一个属性变化时,需要加context上下文作为区分。同时注意remove时也要remove多次,区分context。
static void * SubViewControllerBalanceObserverContext = &SubViewControllerBalanceObserverContext;
static void * ViewControllerBalanceObserverContext = &ViewControllerBalanceObserverContext;
//添加监听器
- (void)setObserver
{
[self addObserver:self forKeyPath:@"account.balance" options:NSKeyValueObservingOptionNew context:SubViewControllerBalanceObserverContext];
}
- (void)setSubObserver
{
[self addObserver:self forKeyPath:@"account.balance" options:NSKeyValueObservingOptionNew context:ViewControllerBalanceObserverContext];
}
//监听器回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if (context == SubViewControllerBalanceObserverContext) {
}
if (context == ViewControllerBalanceObserverContext) {
}
}
如果子类继承父类,同时监听一个属性,会触发两次子类,而不才触发父类,需要在回调里手动去调用:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if (context == SubViewControllerBalanceObserverContext)
{
NSLog(@"SubViewController NewBalance: %lf", self.model.balance);
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
手动触发:
- (void)setBalance:(double)balance
{
//如果新值小于0不触发KVO
if (balance < 0)
{
_balance = balance;
}
else
{
//新值大于0才触发KVO回调函数
[self willChangeValueForKey:@"balance"];
_balance = balance;
[self didChangeValueForKey:@"balance"];
}
}
+ (BOOL)automaticallyNotifiesObserversOfBalance
{
return NO;
}
总结
- 尽量使用静态变量地址作为context,为每个监听都设置context,并使用context移除
- 如果有继承关系,在监听器回调函数中将不是当前类处理的context调用父类的监听器回调函数进行处理
KVO的实现原理
当观察对象A时,KVO会在运行时动态创建A的子类NSKVONotifying_A,并使原来指向A的指针指向它。然后重写其setter方法。新的setter方法会在原setter方法执行前和执行后通知观察者。
在动态生成的子类的setter方法中会调用willChangeValueForKey:
和didChangeValueForKey:
。之后也会调用observeValueForKey:ofObject:change:context:
,继而实现KVO中的回调。
网友评论