KVO键值观察的具体实现
1.KVO简介
KVO是Objective-C对观察者设计模式的一种实现,它提供一种机制,指定一个被观察对象(如A类),当对象中的某个属性发生变化的时候,对象就会接收到通知,并作出相应的处理。在MVC设计架构下的项目,KVO机制很适合实现mode模型和view视图之间的通讯。例如:代码中,在模型类A创建属性数据,在控制器中创建观察者,一旦属性数据发生改变就收到观察者收到通知,通过KVO再在控制器使用回调方法处理实现视图B的更新;
2.实现原理
KVO的实现依赖于Objective-C强大的runtime,KVO的底层实现是监听setter方法。当观察某对象A时,KVO动态机制会动态创建一个A类的子类,并为这个新的子类重写父类的的setter方法。setter方法随后负责通知观察对象属性的变化。
3.深入理解
Apple使用了isa混写(isa-swizzling)来实现KVO,当观察对象A的时候(也就是调用对象A注册观察者的方法的时候),KVO机制会动态创建一个A的新的子类:NSKVONotifying_A的新,该类继承自对象A的本类。且KVO会为NSKVONotifying_A重写其父类的setter方法,setter方法会负责在调用原setter方法之前或之后,通知所有观察对象属性值的更改状况。
addobserver方法内部实现:
在这个方法中,被观察对象的isa指针从指向原来的A类,被KVO机制修改为指向A类的子类-NSKVONotifying_A,来实现当前类属性值改变的监听。
isa指针的作用:每个对象都有一个isa指针,指向该对象的类。它告诉Runtime系统这个对象的类是什么,所以对象注册为观察者的是时候,isa指针会指向新的子类,那么这个对象就会变成新的子类的对象了。因此,该对象调用setter就会调用已经重写的setter了,从而激活键值通知机制。
子类setter方法剖析:
KVO的键值观察通知依赖于NSObject的两个方法:willChangeValueForKey didChangeValueForKey,在存取值的前后分别调用的方法。被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
4.自己实现KVO
首先创建一个NSObject的类扩展,.h文件中自定一个一个方法,由于目标是自定义实现KVO,所以只需要在系统添加观察者的方法名前面添加一个前缀,参数值不变。方法实现如下
- (void)CC_addObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath options:(NSKeyValueObservingOptions)options context:(nullablevoid*)context{ /* 1.自定义一个NSCCKVO_XXX子类 2.重写父类的setter,在内部恢复子类的做法,通知观察者 3.修改self的指针,指向新创建的NSCCKVO_XXX子类 */ //动态生成一个类 NSString*oldClassName =NSStringFromClass([selfclass]); NSString*newClassName = [@"NSCCKVO_"stringByAppendingString:oldClassName]; constchar* name= [newClassNameUTF8String]; //定义一个类 //参数1:继承的那个类参数2:类名称 Class ccClass=objc_allocateClassPair([selfclass], name,0); //子类添加setter方法,以setName为例 class_addMethod(ccClass,@selector(setName:), (IMP)setName,"v@:@"); //注册这个类 objc_registerClassPair(ccClass); //修改self的isa指针 object_setClass(self, ccClass); //绑定observer到self对象中,将观察者绑定当前对象 objc_setAssociatedObject(self, (__bridgeconstvoid*)@"objc", observer,OBJC_ASSOCIATION_RETAIN_NONATOMIC); } voidsetName(idself,SEL_cmd,NSString*newName){ //调用父类的sett方法 Class superClass =class_getSuperclass([selfclass]); //改变isa指针。为父类,调用set方法 object_setClass(self, superClass); objc_msgSend(self,@selector(setName:),newName); //拿出观察者 idobserver =objc_getAssociatedObject(self, (__bridgeconstvoid*)@"objc"); //通知观察者 objc_msgSend(observer,@selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"new":newName},nil); //改回子类类型 object_setClass(self, [selfclass]); }
然后在ViewController导入NSObject的类扩展的头文件
Person*person = [[Personalloc]init]; _person= person; //[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]; // 这个方法就是刚才自己去实现的那个方法[personCC_addObserver:selfforKeyPath:@"name"options:NSKeyValueObservingOptionNewcontext:nil]; -(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{ NSLog(@"%@的%@值变成了%@",object,keyPath,change[@"new"]); }
此处,自己实现KVO就完成了。观察者观察的是属性,只有遵循 KVO 变更属性值的方式才会执行KVO的回调方法,例如是否执行了setter方法、或者是否使用了KVC赋值。如果赋值没有通过setter方法或者KVC,而是直接修改属性对应的成员变量,例如:仅调用_name = @"newName",这时是不会触发kvo机制,更加不会调用回调方法的。所以使用KVO机制的前提是遵循 KVO 的属性设置方式来变更属性值。