介绍KVO和KVC
一、什么是kvc
kvc 是 Key-Value-Coding 的简称。
kvc 是一种可以直接通过字符串的名字 key 来访问类属性的机制,而不是通过调用 setter、getter 方法去访问。
我们可以通过在运行时动态的访问和修改对象的属性。而不是在编译时确定,kvc 是 iOS 开发中的黑魔法之一。
1.1 kvc基本语法
获取值
valueForKey: 传入NSString属性的名字。
valueForKeyPath: 属性的路径,xx.xx
valueForUndefinedKey: 默认实现是抛出异常,可重写这个函数做错误处理
修改值
setValue:forKey:
setValue:forKeyPath:
setValue:forUnderfinedKey:
setNilValueForKey: 对非类对象属性设置nil时调用,默认抛出异常
1.2 kvc的工作原理
直接找setter方法
找property Ivar,进行object_setIvar
找 _property Ivar,进行Ivar赋值
setValue: forUndefinedKey
分析1: 假如我们调用 [[NSObject alloc] setValue:nil forKey:@"property"],其 kvc 调用如下所示:
去模型中查找有没有对应的 setter 方法:例如:setProperty 方法,有就直接调用这个 setter 方法给属性赋值;
如果找不到 setter 方法,接着就会去寻找有没有 property Ivar,如果有,就直接进行 void object_setIvar ( id obj, Ivar ivar, id value ) 赋值;
如果找不到 property 属性,接着又会去寻找 _property Ivar,如果有,直接进行 Ivar 赋值
如果都找不到会调用 setValue: forUndefinedKey:, 然后报出如下所示的崩溃信息。
1.3 kvc例子
用一般的 setter 和 getter,在类外部是不能访问到私有变量的,不能设值给只读变量 用kvc突破限制
Teacher *teacher = [Teacher new];
// name在类Teacher是只读,用kvc设置name值
// 设置 readonly value
[teacher setValue:@"Jack" forKey:@"name"];
// age在类Teacher是私有变量,用kvc设置age值
// 设置 private value
[teacher setValue:@24 forKey:@"age"];
// 获取 readonly value
NSLog(@"name: %@", [teacher valueForKey:@"_name"]);
// 获取 private value
NSLog(@"age: %d", [[teacher valueForKey:@"_age"] intValue]);
修改 TextField 的 placeholder的私有属性
[_textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[_textField setValue:[UIFont systemFontOfSize:14] forKeyPath:@“_placeholderLabel.font"];
修改 UIPageControl 的图片私有属性
[_pageControl setValue:[UIImage imageNamed:@"selected"] forKeyPath:@"_currentPageImage"];
[_pageControl setValue:[UIImage imageNamed:@"unselected"] forKeyPath:@"_pageImage"];
1.4 key和keypath的区别
key :只能接受当前类所具有的属性,不管是自己的,还是从父类继承过来的,如view.setValue(CGRectZero(),key: "frame");
keypath: 除了能接受当前类的属性,还能接受当前类属性的属性,即可以接受关系链,如view.setValue(5,keypath: "layer.cornerRadius")
1.5 kvc访问属性和用点语法访问属性的区别
用点语法编译器会做预编译检查,访问不存在的属性编译器会报错,但是用 kvc 方式编译器无法做检查,如果有错误只能运行的时候才能发现(crash)。
相比点语法用 kvc 方式 kvc 的效率会稍低一点,但是灵活{coderiding解析:灵活的意思是当你在编译前不确定你要访问什么属性,而要等到运行的时候,根据具体情况而言来访问,这时候kvc就有它的价值},可以在程序运行时决定访问哪些属性。
用 kvc 可以访问对象的私有成员变量。
二、kvo的本质
kvo 的本质就是监听对象的属性进行赋值的时候有没有调用 setter 方法
kvo 是 Key-Value-Observing 的简称。
kvo 是一个观察者模式。观察一个对象的属性,注册一个指定的路径,若这个对象的的属性被修改,则 kvo 会自动通知观察者。
更通俗的话来说就是任何对象都允许观察其他对象的属性,并且可以接收其他对象状态变化的通知。
每次对象的属性被改变后,那么监听者就会收到通知
kvo 是通知的一种,还有的通知就是 NSNotificationCenter
在ObjC中要实现kvo则必须实现NSKeyValueObServing协议,不过幸运的是NSObject已经实现了该协议,因此几乎所有的ObjC对象都可以使用kvo。
一个对象,可以被多个观察,所以每次销毁的时候就要移除观察
kvo 是一个对象能观察另一个对象属性的值,kvo
适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。kvo 只能对属性做出反应,不会用来对方法或者动作做出反应。
优点:
提供一个简单的方法来实现两个对象的同步。
能够提供观察的属性的新值和旧值。
每一次属性值改变都是自动发送通知,不需要开发者手动实现。
用 keypath 来观察属性,因此也可以观察嵌套对象。
缺点:
观察的属性必须使用字符串来定义,因此编译器不会出现警告和检查
只能重写回调方法来后去通知,不能自定义 selector。当观察多个对象的属性时就要写"if"语句,来判断当前的回调属于哪个对象的属性的回调。
2.1 kvo原理:
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。 派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
2.2 kvo语法
//【注册】1.注册观察者,实施监听
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
//【观察】2.观察方法,回调方法,在这里处理属性发生的变化;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
//【移除】3.移除观察者;
[self removeObserver:self forKeyPath:@“age"];
2.3 kvo观察例子
_person = [[Person alloc] init];
/**
* 添加观察者
* @param observer 观察者
* @param keyPath 被观察的属性名称
* @param options 观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)
* @param context 上下文,可以为nil。
*/
[_person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
/**
* kvo 回调方法
*
* @param keyPath 被修改的属性
* @param object 被修改的属性所属对象
* @param change 属性改变情况(新旧值)
* @param context context 传过来的值
*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
{
NSLog(@"%@ 对象的%@属性改变了:%@",object,keyPath,change);
}
/**
* 移除观察者
*/
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"age"];
}
监听 ScrollView 的 contentOffSet 属性
[scrollview addObserver:self
forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionNew
context:nil];
kvo选择不监听某个属性
// 通知 key 的观察者
+ ( BOOL ) automaticallyNotifiesObserversForKey: ( NSString * ) key{
// 不观察这个 key
if ([ key isEqualToString:@"link" ]) {
return NO ;
}
// 调用系统的方法
return [ super automaticallyNotifiesObserversForKey:key ];
}
kvo通过po查看被观察对象的所有观察信息
// 这会打印出有关谁观察谁之类的很多信息。
(lldb) po [observedObject observationInfo]
2.4 kvo和线程
一个需要注意的地方是,kvo 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用 -didChange... 会触发 kvo 通知。
所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 kvo 通知。通常来说,我们不推荐把 kvo 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 kvo。
kvo 是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),kvo 会保证下列两种情况的发生:
首先,如果我们调用一个支持 kvo 的 setter 方法,如下所示:
self.exchangeRate = 2.345;
kvo 能保证所有 exchangeRate 的观察者在 setter 方法返回前被通知到。
其次,如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 选项,直到 -observe... 被调用之前, exchangeRate 的 accessor 方法都会返回同样的值。
2.5 kvo使用注意
首先,KVO 兼容是 API 的一部分。如果类的所有者不保证某个属性兼容 KVO,我们就不能保证 KVO 正常工作。苹果文档里有 KVO 兼容属性的文档。例如,NSProgress 类的大多数属性都是兼容 KVO 的。
当做出改变以后,有些人试着放空的 -willChange 和 -didChange 方法来强制 KVO 的触发。KVO 通知虽然会生效,但是这样做破坏了有依赖于 NSKeyValueObservingOld 选项的观察者。详细来说,这影响了 KVO 对观察键路径 (key path) 的原生支持。KVO 在观察键路径 (key path) 时依赖于 NSKeyValueObservingOld 属性。
我们也要指出有些集合是不能被观察的。KVO 旨在观察关系 (relationship) 而不是集合。我们不能观察 NSArray,我们只能观察一个对象的属性——而这个属性有可能是 NSArray。举例说,如果我们有一个 ContactList 对象,我们可以观察它的 contacts 属性。但是我们不能向要观察对象的 -addObserver:forKeyPath:... 传入一个 NSArray。
相似地,观察 self 不是永远都生效的。而且这不是一个好的设计。
2.6 kvo底层原理剖析
/// 创建person对象,监听person的age属性
- (void)configurePesonClass {
self.person = [[Person alloc] init];
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
剖析:
self.person = [[Person alloc] init];
系统会动态创建一个继承于 Person 的 NSKVONotifying_Person
person 的 isa 指针指向的类 Person 变成 NSKVONotifying_Person,所以接下来的 person.age = newAge 的时候,他调用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子类)的 setter 方法
重写NSKVONotifying_Person的setter方法:[super setName:newName]
通知观察者告诉属性改变。
派生类 NSKVONotifying_Person 剖析:
在这个过程,被观察对象的 isa 指针从指向原来的 Person 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_Person 类,来实现当前类属性值改变的监听。
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为 NSKVONotifying_Person 的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_Person 的中间类,并指向这个中间类了。
因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。这也是 KVO 回调机制,为什么都俗称 KVO 技术为黑魔法的原因之一吧:内部神秘、外观简洁。
- (void)setName:(NSString *)newName
{
[self willChangeValueForKey:@"name"]; // KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; // 调用父类的存取方法
[self didChangeValueForKey:@"name"]; // KVO 在调用存取方法之后总调用
}
三、kvc和kvo的keyPath一定是属性么?
kvc 支持实例变量
kvo 只能手动支持手动设定实例变量的kvo实现监听
Last updated
Was this helpful?