描述
KVO全称
KeyValueObserving
。俗称“键值监听”。
利用Key来找到某个对象并监听其属性的改变。也是一种典型的观察者模式。
在某个对象注册监听者后,被监听对象的属性发生改变时,会发送一个通知给监听者。以便监听者执行回调操作。
本文演示代码地址
KVO方法介绍
1、通过addObserver:forKeyPath:options:context:
方法注册观察者。
/**
添加KVO监听
@param observer 添加观察者,被观察者属性变化通知的目标对象
@param keyPath 监听的属性路径
@param options 监听类型 - options支持按位或来监听多个事件类型
@param context 监听上下文context主要用于在多个监听器对象监听相同keyPath时进行区分
*/
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
2、通过 observeValueForKeyPath:ofObject:change:context:
获得回调,从而做出事件处理。
/**
监听器对象的监听回调方法
@param keyPath 监听的属性路径
@param object 被观察者
@param change 监听内容的变化
@param context context为监听上下文,由add方法回传
*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context;
3、当观察者不需要监听时,调用可以removeObserver:forKeyPath:
方法将KVO移除。需要注意的是:调用removeObserver
需要在观察者消失之前,否则会导致Crash。
- (void)dealloc{
[self removeObserver:self forKeyPath:@"keyFlag"];
}
简单示例
@interface ViewController ()
@property (nonatomic,strong) Animal * ani;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ani = [[Animal alloc] init];
self.ani.age = 10;
// 添加键值监听
[self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}
// 点击事件,触发属性修改
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.ani.age += 5;
}
// 获得回调,实时监听属性改变、
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到了%@对象的%@属性由%@变成了%@属性",object,keyPath,change,change);
}
// 需要在不使用的时候,移除监听
- (void)dealloc{
[self.ani removeObserver:self forKeyPath:@"age"];
}
@end
KVO原理探究
1、利用RuntimeAPI动态生成一个子类
NSKVONotifying_XXX
,并且让当前instance对象
的isa指针
指向这个全新子类。
2、当修改instance对象的属性时,会触发setter方法,调用Foundation的_NSSetXXXValueAndnotify
函数
- 调用
willChangeValueForKey:
- 调用原来的
setter
实现(父类原来的setter方法)- 调用
didChangeValueForKey
此时内部触发监听器(Oberser)的监听方法- observeValueForKeyPath: ofObject: change: context:
代码验证上述流程
第一步:通过runtime查看isa指针指向的 class对象
如果观察 Animal的age属性。
系统会在运行时生成NSKVONotifying_Animal
在NSKVONotifying_Animal中重写setter
、class
、dealloc
等方法。
使Animal实例对象的isa指针指向NSKVONotifying_Animal
NSKVONotifying_Animal
的superclass指向Animal
探究过程
// 注册成为观察者
NSLog(@"添加KVO之前,Animal的class是 = %s",object_getClassName(self.ani));
[self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加KVO之后,Animal的class是 = %s",object_getClassName(self.ani));
结果如下:
添加KVO之前,Animal的class是 = Animal
添加KVO之后,Animal的class是 = NSKVONotifying_Animal
注册成为观察者之后,类变成了NSKVONotifying_Animal
而再是 Animal
。
我们先看一下NSKVONotifying_Animal
类内部的方法。
#import
//打印某个类中的所有方法
- (void)printMethonNamesFromClass:(Class)cls{
unsigned int count;
//获取方法列表
Method *methodList = class_copyMethodList(cls, &count);
//保存方法名
NSMutableString *methonNames = @"".mutableCopy;
for (int i = 0; i < count; i++) {
//获取方法
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methonNames appendFormat:@"%@", [NSString stringWithFormat:@"%@, ",methodName]];
}
NSLog(@"methonNames = %@",methonNames);
//c语音创建的list记得释放
free(methodList);
}
结果如下:
[self printMethonNamesFromClass:object_getClass(self.ani)];
----------------------------------------------
methonNames = setAge:, class, dealloc, _isKVOA,
画图分析KVO内部结构
第二步:- (void)setAge:(int)age方法
为了比较在注册观察者前后setter方法的变化,我们新创建一个实例ani1
self.ani = [[Animal alloc] init];
self.ani1 = [[Animal alloc] init];
NSLog(@"添加KVO之前,ani的setAge是 = %p,未添加KVO的ani1的setAge是 = %p",
[self.ani methodForSelector:@selector(setAge:)],
[self.ani1 methodForSelector:@selector(setAge:)]);
// 注册成为观察者
[self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加KVO之后,ani的setAge是 = %p,未添加KVO的ani1的setAge是 = %p",
[self.ani methodForSelector:@selector(setAge:)],
[self.ani1 methodForSelector:@selector(setAge:)]);
结果如下:
添加KVO之前,ani的setAge是 = 0x10d751460,未添加KVO的ani1的setAge是 = 0x10d751460
添加KVO之后,ani的setAge是 = 0x10daaacf2,未添加KVO的ani1的setAge是 = 0x10d751460
这里可以看到,添加KVO前后,setAge方法有所改变
我们进入debugger来看看这第这个方法的实现到底是怎样的:
(gdb) print (IMP) 0x10daaacf2
$1 = (IMP) 0x96a1a550 <_NSSetIntValueAndNotify>
原来在重写的NSKVONotifying_Animal
的-setAge
方法中会调用_NSSetIntValueAndNotify:
// 注:Foundation框架中类似_NSSetIntValueAndNotify的方法实现还有很多:
__NSSetBoolValueAndNotify
__NSSetCharValueAndNotify
__NSSetDoubleValueAndNotify
__NSSetFloatValueAndNotify
__NSSetIntValueAndNotify
__NSSetLongLongValueAndNotify
__NSSetLongValueAndNotify
__NSSet0bjectValueAndNotify
__NSSetPointValueAndNotify
__NSSetRangeValueAndNotify
__NSSetRectValueAndNotify
__NSSetShortValueAndNotify
__NSSetSizeValueAndNotify
查看_NSSet*ValueAndNotify的内部实现
- (void)setAge:(int)age{
_NSSet*ValueAndNotify();
}
// 因为_NSSetIntValueAndNotify在Foundation框架中,无法查看起具体实现,根据实践猜测大致为代码如下:
void _NSSet*ValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
//通过监听器,监听属性发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
在Animal类中验证
#import "Animal.h"
@implementation Animal
//Animal内部代码实现
- (void)setAge:(int)age{
_age = age;
NSLog(@"setAge");
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey == begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey == end");
}
@end
结果如下:
kvoAndkvoDemo[2391:50226] willChangeValueForKey
kvoAndkvoDemo[2391:50226] setAge
kvoAndkvoDemo[2391:50226] didChangeValueForKey == begin
kvoAndkvoDemo[2391:50226] 监听到了对象的age属性由{
kind = 1;
new = 15;
old = 10;
}变成了{
kind = 1;
new = 15;
old = 10;
}属性
kvoAndkvoDemo[2391:50226] didChangeValueForKey == end
如果不添加监听,则不会执行
willChangeValueForKey
和didChangeValueForKey
方法、验证成功!
汇总
1、当你观察一个对象时,系统通过Runtime动态的创建一个该类的派生类,这个类继承自该对象的原本的类,并了重写被观察属性的setter方法。
2、isa指针会指向这个新创建的类,该对象就变成新创建子类的实例了、
3、重写的setter方法,执行_NSSet*ValueAndNotify,会负责在调用原来的setter方法前后,通知所有观察对象:值的改变。
拓展思考
1、用法听起来和
NSNotification
很相似啊, 其实NSNotification
也是观察者模式,但是NSNotification
是一种广播机制,KVO是被观察者直接发消息给观察者,是对象间的相互沟通。NSNotification
则是两者都和通知中心对象交互,对象之间不知道彼此。
2、KVO行为是同步的,并且发生与观察的值发生在同样的线程上,没有队列或Run-Loop
处理。【使用注意】
用途
常见运用是监听
ScrollView
的contentOffset
属性。当用户滚动结束时动态改变某些空间的实现效果。下拉刷新,渐变导航栏,头像变大缩小等。
KVC
KVC (Key-Value-Coding )键值编码。顾名思义:可以通过一个Key来访问某个属性。
常用方法
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *) keyPath;
简单示例
self.ani = [[Animal alloc] init];
[self.ani setValue:@38 forKey:@"age"];
NSLog(@"%d",self.ani.age);
self.ani1 = [[Animal alloc] init];
[self.ani1 setValue:@99 forKeyPath:@"cat.weight"];
NSLog(@"%d",self.ani1.cat.weight);
self.ani2 = [[Animal alloc] init];
self.ani2.age = 10;
NSLog(@"%@",[self.ani2 valueForKey:@"age"]);
setValue:forKey:的原理:
当我们设置setValue:forKey:时
首先会查找setKey:、_setKey: (按顺序查找)
如果有直接调用
如果没有,先查看accessInstanceVariablesDirectly方法
如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量
找到直接复制
未找到报错NSUnkonwKeyException错误
valueForKey:的原理:
kvc取值按照 getKey、key、iskey、_key 顺序查找方法
存在直接调用
没找到同样,先查看accessInstanceVariablesDirectly方法
如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量
找到直接复制
未找到报错NSUnkonwKeyException错误
思考
我们可以通过 self.ani.age = 10; 来赋值,也可通过上述代码进行赋值,看着多此一举、
可是如果人这个类的属性是没有暴露在外面呢?比如现在给人这个类一个私有的身高的属性。就可以通过KVC进行赋值、
Key 和 KeyPath区别接联系
Key:只能访问当前对象的属性,如果按路径找会报错。
KeyPath:相当于根据路径去寻找属性,能利用运算符一层一层往内部访问属性。
用途
我们通过KVC可以直接对私有属性并进行赋值
字典转模型
拓展
我们通过
XIB
或者SB
拖线布局连线错误的时候也会报错说找不到什么key,说明Storyboard
在赋值的时候也是通过KVC来操作的。
试题
KVO相关:
1. iOS用什么方式来实现对一个对象的KVO?(KVO的本质是什么?)
2. 如何手动出发KVO?
3. 直接修改成员变量会触发KVO么?
KVC相关:
1. 通过KVC修改属性会触发KVO么?
2. KVC的赋值和取值过程是怎样的?原理是什么?