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