KVC && KVO

一、什么是KVC?

KVC(Key-value coding)键值编码,它提供了一种通过key间接访问对象的属性或成员变量的方法,而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性或成员变量,而不是在编译时确定。

KVC的使用环境:

无论是property还是普通的全局属性变量,都可以使用KVC;

KVC优点:

  • 主要的好处就是减少代码量;
  • 没有property的变量(即:私有变量private)也能通过KVC进行设置。

KVC缺点:

如果key只写错,编写的时候不会报错,但是运行的时候会报错;

KVC使用的基本方法:

 //默认返回YES,表示如果没有找到Set<Key>方法的话,
 //会按照_key,_iskey,key,iskey的顺序搜索成员,
 //设置成NO就不这样搜索
 + (BOOL)accessInstanceVariablesDirectly;
 
 //KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、
 //为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
 - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
 
 //这是集合操作的API,里面还有一系列这样的API,
 //如果属性是一个NSMutableArray,那么可以用这个方法来返回。
 - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
 
 //如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,
 //则会调用这个方法,默认是抛出异常。
 - (nullable id)valueForUndefinedKey:(NSString *)key;
 
  //和上一个方法一样,但这个方法是设值。
 - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

 //如果你在SetValue方法时面给Value传nil,则会调用这个方法
 - (void)setNilValueForKey:(NSString *)key;
 
 //输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

二、KVC设值

假设在一个 TestOjb 类中,有一个成员变量 name 表示姓名,正常情况下,在外界是无法设置或获取其值的。那么用KVC是如何实现的呢?KVC要设值,是根据对象中的对应的 key 来决定的。KVC在内部又是按什么样的顺序来寻找 key 的呢?

当调用 setValue:@" " forKey:@" " 方法时,底层的执行机制如下:

1、首先搜索是否有 setKey:的方法(Key是成员变量名), 如果没有则会继续依次搜索是否有_setKey: 、 setIsKey:的方法(首先搜索setter方法);

2、如果上述方法仍没有找到,此时会调用 + (BOOL)accessInstanceVariablesDirectly 方法(是否直接访问成员变量方法)。

该方法默认返回YES,会按照_key,_iskey,key,iskey的顺序搜索成员变量名。

若返回NO,则直接调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key 方法(默认是抛出异常)。

#import "TestObj.h"

@interface TestObj()
{
    NSString *_name;  //优先级1
    NSString *_isName; //优先级2
    NSString *name; //优先级3
    NSString *isName; //优先级4
    
}
@end

@implementation TestObj



- (void)setName:(NSString *)name{//优先级1
    NSLog(@"setName");
}
- (void)_setName:(NSString *)name{//优先级2
    NSLog(@"_setName");
}
- (void)setIsName:(NSString *)name{//优先级3
    NSLog(@"setIsName");
}

- (id)valueForKey:(NSString *)key{
    NSLog(@"_name:%@", _name);
    NSLog(@"_isName:%@", _isName);
    NSLog(@"name:%@", name);
    NSLog(@"isName:%@", isName);
    return nil;
}

+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"setValue异常");
}

- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"valueForUndefinedKey异常");
    return nil;
}


@end
    TestObj *obj = [[TestObj alloc]init];
    [obj setValue:@"小明" forKey:@"name"];
    NSString *name = [obj valueForKey:@"name"];

通过对代码的注释打印可得优先级正好对应上述搜索机制。

三、KVC取值

当调用 valueForKey:@" " 方法时,底层的执行机制如下:

1、按先后顺序搜索 getKey:、key、isKey(getter方法)三个方法,若某一个方法被实现,取到的即是方法返回的值,后面的方法不再运行。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。

2、如果上面的getter方法没有找到,则会调用+ (BOOL)accessInstanceVariablesDirectly方法判断是否允许取成员变量的值。

若返回NO,直接调用- (nullable id)valueForUndefinedKey:(NSString *)key方法,默认是奔溃。 

若返回YES,会按先后顺序取_key、_isKey、 key、isKey的值。

3、返回YES时,_key、_isKey、 key、isKey 的值都没取到,调用 - (nullable id)valueForUndefinedKey:(NSString *)key 方法。

 四、什么是KVO?

KVO 即 Key-Value Observing,键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。也就是说KVO可以通过监听key,来获得value的变化,用来在对象之间监听状态变化。

KVO优点:

  • 能够提供一种简单的方法实现两个对象的同步;
  • 能够对内部对象的状态改变作出响应,而且不需要改变内部对象的实现;
  • 能够提供被观察者属性的最新值和之前的值;
  • 使用key Path来观察属性,因此可以观察嵌套对象;
  • 完成了对观察对象的抽象,因为不需要额外的代码来允许观察者被观察。

KVO缺点:

  • 我们观察的属性必须使用strings定义,编译时不会出现警告;
  • 对属性重构,将导致观察代码不可用;
  • 复杂的 “if” 语句要求对象正在观察多个值,是因为所有的观察代码通过一个方法来指向;
  • 当释放观察者的时候不需要移除观察者。

观察者对象与被观察者对象注册与解除注册监听方法

observer:观察者,也就是KVO通知的订阅者。订阅着必须实现 
keyPath:描述将要观察的属性,相对于被观察者。
options:KVO的一些属性配置;有四个选项。
context: 上下文,这个会传递到订阅着的函数中,用来区分消息,所以应当是不同的。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

options所包括的内容
NSKeyValueObservingOptionNew:change字典包括改变后的值
NSKeyValueObservingOptionOld:change字典包括改变前的值
NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)

处理变更通知

每当监听的keyPath发生变化了,就会在这个函数中回调。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

在这里,change 这个字典保存了变更信息,具体是哪些信息取决于注册时的 NSKeyValueObservingOptions。

系统实现KVO有以下几个步骤:

  • 当类A的对象第一次被观察的时候,系统会在运行期动态创建类A的派生类。我们称为B。
  • 在派生类B中重写类A的setter方法,B类在被重写的setter方法中实现通知机制。
  • 类B重写会 class方法,将自己伪装成类A。类B还会重写dealloc方法释放资源。
  • 系统将所有指向类A对象的isa指针指向类B的对象。
#pragma mark - KVO
- (void)TestForKVO{
    self.objs = [[TestObj alloc]init];
    self.objs.phone = @"1234";
    //此行注册监听后,objs由TestObj类变成NSKVONotyfing_TestObj类。
    [self.objs addObserver:self forKeyPath:@"phone" options:NSKeyValueObservingOptionNew context:nil];
   
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@监听到%@属性的改变为%@", object, keyPath, change[@"new"]);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.objs.phone = @"kvo实现啦";
}

- (void)dealloc{
    [self.objs removeObserver:self forKeyPath:@"phone"];
}

手动实现键值观察

@interface TestObj : NSObject

- (void)setAge:(int)age;
- (int)age;


@end
#pragma mark - KVO
- (id)init{
    self = [super init];
    if (self) {
        _age = 20;
    }
    return self;
}

- (int)age{
    return _age;
}
- (void)setAge:(int)age{
    [self willChangeValueForKey:@"age"];
    _age = age;
    [self didChangeValueForKey:@"age"];
}

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;
其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。

 

  • 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
  • 派生类在被重写的 setter 方法中实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
  • 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

KVO与Notification之间的区别:

notification是需要一个发送notification的对象,一般是notificationCenter,来通知观察者。

KVO是直接通知到观察对象,并且逻辑非常清晰,实现步骤简单。

 
 

参考:https://www.jianshu.com/p/9183365170bd

   https://www.jianshu.com/p/b9f020a8b4c9

     https://www.cnblogs.com/junhuawang/p/5802325.html

GitHub:https://github.com/hongsheng1024/RunTime


 

posted @ 2018-10-26 23:03  hongsheng  阅读(190)  评论(0编辑  收藏  举报