KVC与KVO

作者: 夜雨聲煩_ | 来源:发表于2018-03-24 17:32 被阅读0次

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中的回调。

相关文章

网友评论

      本文标题:KVC与KVO

      本文链接:https://www.haomeiwen.com/subject/jbgqcftx.html