一、什么是KVO?
KVO(key-value observing)是Objective-C对观察者设计模式的一种实现。【另一种是:通知机制(notification),详情参考:iOS 趣谈设计模式——通知】;
KVO提供一种机制,指定一个被观察的对象(A类),当对象某个属性(A中的属性name)发生更改时,对象会获得通知,并作出相应处理;【且不需要给被观察的对象添加任何额外代码,就能使用KVO机制】
KVO在MVC设计架构下的项目很适合实现mode模型和view视图指尖的通讯。
例如:代码中,在模型类A创建属性数据,在控制器中创建观察者,一旦属性数据发生改变就收到观察者收到通知,通过KVO再在控制器使用回调方法处理实现视图B的更新。
二、实现原理
KVO在Apple中的API文档如下:
Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered foran attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate classrather than at the trueclass…
KVO的实现依赖于Runtime,Apple的文档对于KVO机制的实现细节没有过多的描述,但是我们可以通过Runtime的所提供的发放区探索【可参考:Runtime的几个小例子】,关于KVO机制的底层实现原理:
基本原理:
1、KVO是关于runtime机制实现的
2、当某个类的对象属性第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的setter方法内实现真正的通知机制
3、如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
4、每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统就会偷偷讲isa指针指向动态生成的派生类,从而在给被监控属性复制是执行的是派生类的setter方法
5、键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:,在一个被观察属性发生改变之前,willChangeValueForkey:和didChangeValueForKey:;在一个被观察属性发生改变之前,willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而observeValueForKey:ofObject:change:context:也会被调用
KVO深入原理:
Apple使用了isa混写(isa-swizzling)来实现KVO。当观察对象A时,KVO机制动态创建一个新的名为NSKVONotifying_A的新类,该类集成字对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter方法,setter方法会负责在调用元setter方法之前和之后,通知所有观察对象属性值的更改情况。(备注:isa混写(isa-swizzling)isa:is a kind of ; swizzling: 混合,搅合)
1、NSKVONotifying_A类剖析:在这个过程,被观察对象的isa指针从指向原来的A类,被KVO机制修改为指向系统创建的自雷NSKVONotifying_A类,来实现当前类属性值改变的监听;
所以当我们从应用层面来看,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层想实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类,就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。
(isa指针的作用:每个对象都有isa指针,指向该对象的类,他告诉Runtime系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。)因而在该对象上对setter的调用就会调用已重写的setter,从而激活键值通知机制。
2、子类setter方法剖析:KVO的键值观察通知依赖与NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:,在存取数值的前后分别调用2个方法:
被观察属性发生改变之前,willChangeValueForkey:被调用,通知系统该keyPath的属性值即将变更;当改变发生后,didChangeValueForkey:被调用,通知系统该keyPath的属性值已经变更;之后,observeValueForKey:ofObject:context:也会被调用。且重写观察属性的setter方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
– void)setName: NSString*)newName{ [ selfwillChangeValueForKey: @”name”]; //KVO 在调用存取方法之前总调用 [ supersetValue:newName forKey: @”name”]; //调用父类的存取方法 [ selfdidChangeValueForKey: @”name”]; //KVO 在调用存取方法之后总调用 }
三、特点:
观察者观察的是属性,只有遵循KVO变更属性值的方式才会执行KVO的回调方法,例如是否执行了setter方法、或者是否使用了KVC赋值。
如果赋值没有通过setter方法或者KVC,而是直接修改属性对应的成员变量,例如:仅调用_name = @”newName”,这时是不会触发KVO机制,更加不会调用回调方法的。
所以使用KVO机制的前提是遵循KVO的属性设置方式来变更属性值。
【应用部分】
四、步骤
1、注册观察者,实施监听
2、在回调方法中处理属性发生的变化
3、移除观察者
五、实现方法(苹果API文档中的方法)
A.注册观察者: //第一个参数 observer:观察者 (这里观察self.myKVO对象的属性变化) //第二个参数 keyPath: 被观察的属性名称这里观察 self.myKVO 中 num 属性值的改变) //第三个参数 options: 观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项) //第四个参数 context: 上下文,可以为 KVO 的回调方法传值(例如设定为一个放置数据的字典) [ self.myKVO addObserver: selfforKeyPath: @”num”options: NSKeyValueObservingOptionOld| NSKeyValueObservingOptionNewcontext: nil]; B.属性keyPath)的值发生变化时,收到通知,调用以下方法: //keyPath:属性名称 //object:被观察的对象 //change:变化前后的值都存储在 change 字典中 //context:注册观察者时,context 传过来的值 – void)observeValueForKeyPath: NSString*)keyPath ofObject: id)object change: NSDictionary< NSString*, id> *)change context: void*)context { }
六、上代码:
1.新建项目
UI界面设计如下:
第一个是便签,用于显示num数值,关联ViewController并命名:label;
第二个是按钮,用于改变num的数值,关联ViewController并命名为:changeNum。
2.模型创建
【新建一个File,选择Cocoa Touch Class,命名为“myKVO”,记得选择Subclass of “NSObject”.】代码如下:
(myKVO.h):
@interface myKVO : NSObject
@propertynonatomic,assign) int num;//属性设置为int类型的
@end
(myKVO.m):
#import “myKVO.h”
@implementation myKVO
@synthesize num;
@end
3.在ViewController中监听并相应属性改变
(ViewController.h):
#import <UIKit/UIKit.h>
@interfaceViewController: UIViewController @property weak, nonatomic) IBOutlet UILabel*label; //便签 label – IBAction)changeNum: UIButton*)sender; //按钮事件 @end
ViewController.m):
#import “ViewController.h” #import “myKVO.h” @interfaceViewController) @property nonatomic, strong)myKVO *myKVO; @end @implementationViewController – void)viewDidLoad { [ superviewDidLoad]; s elf.myKVO = [[myKVO alloc]init]; /*1.注册对象myKVO为被观察者: option中, NSKeyValueObservingOptionOld 以字典的形式提供 “初始对象数据”; NSKeyValueObservingOptionNew 以字典的形式提供 “更新后新的数据”; */ [ self.myKVO addObserver: selfforKeyPath: @”num”options: NSKeyValueObservingOptionOld| NSKeyValueObservingOptionNewcontext: nil]; } /* 2.只要object的keyPath属性发生变化,就会调用此回调方法,进行相应的处理:UI更新:*/ – void)observeValueForKeyPath: NSString*)keyPath ofObject: id)object change: NSDictionary< NSString*, id> *)change context: void*)context{ // 判断是否为self.myKVO的属性“num”: if[keyPath isEqualToString: @”num”] && object == self.myKVO) { // 响应变化处理:UI更新(label文本改变) self.label.text = [ NSStringstringWithFormat: @”当前的num值为:%@”, [change valueForKey: @”new”]]; //change的使用:上文注册时,枚举为2个,因此可以提取change字典中的新、旧值的这两个方法 NSLog @”\\noldnum:%@ newnum:%@”,[change valueForKey: @”old”], [change valueForKey: @”new”]); } } /*KVO以及通知的注销,一般是在-void)dealloc中编写。 至于很多小伙伴问为什么要在didReceiveMemoryWarning?因为这个例子是在书本上看到的,所以试着使用它的例子。 但小编还是推荐把注销行为放在-void)dealloc中。严肃脸