KVO实现原理和具体应用

一、什么是KVO?

KVOkey-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实现原理和具体应用_第1张图片


三、特点:

观察者观察的是属性,只有遵循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。

KVO实现原理和具体应用_第2张图片

2.模型创建

【新建一个File,选择Cocoa Touch Class,命名为“myKVO”,记得选择Subclass of "NSObject".】代码如下:

(myKVO.h):

@interface myKVO : NSObject

@property(nonatomic,assign) int num;//属性设置为int类型的

@end

(myKVO.m):

#import "myKVO.h"

@implementation myKVO

@synthesize num;

@end

3.在ViewController中监听并相应属性改变

(ViewController.h):

#import 

@interfaceViewControllerUIViewController
@property( weaknonatomicIBOutlet 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中。(严肃脸

你可能感兴趣的:(KVO)