KVO原理
简述:
KVO是一种实现键值观察的手段,其本质是为被观察的类创建一个子类名为:(类名)_Notifying(这里以MyObj为例,那么系统会为你创建一个MyObj_Notifying类,继承自MyObj),在这个子类中重写相对应属性的set方法,在set方法中修改属性值之前调用了形如valueWillChange这样的方法,在修改属性值之后调用了形如valueDidchange方法。而当你在创建一个实例的时候,runtime会偷偷把你的类换成他为你强化过的类,从而使你的Myobj引用指向一个MyObj_Notifying对象(偷偷摸摸),这个对象除了set方法,都与你创建的类一模一样,因此在修改值的时候会触发观察方法valueWillChange和valueDidChange。
由此可以看出,通过一些不经由set方法修改属性值的时候,KVO不会起作用,例如在某些函数中出现:_num = 10;类似的语句,KVO是无法检测到的(因为没走set方法),但是属性值确实是改变了,因此在除了get方法和set方法中以外的其他的地方最好使用self.num而不是_num以避免不必要的bug。在之后自行实现的KVO中会对此做出测试。
具体:
- 在此,我将创建一个具体例子,以实现自己的KVO。在通常情况下时候,这些过程都是由runtime在运行时做的。
- 首先创建测试类,Test_Obj
@interface Test_Obj : NSObject
@property(nonatomic, copy)NSString *str;
@property(nonatomic, assign)NSInteger num;
- (void)changeToZero;//这个方法用于测试使用_str,_num修改属性时,KVO是否依然生效。
@end
@implementation Test_Obj
- (void)changeToZero{
_str = [NSString stringWithFormat:@"Zero"];
_num = 0;
}
@end
- 如果我们现在希望为这个类添加一个KVO,需要制定一个统一的标准以使得对所有的类都可以使用此方法添加KVO,创建一条添加KVO的统一协议。
@protocol LWNotifyingProtocol
@optional
- (void)property:(NSString *)propertyName valueWillChange:(id)oldValue newValue:(id)newValue;
- (void)property:(NSString *)propertyName valueDidChange:(id)oldValue newValue:(id)newValue;
@end
这个协议中的方法将作为观察者的回调。被观察者将通过这两个回调通知观察者值发生了改变,propertyName参数是改变的属性名,通过runtime获取属性名传出,在原生KVO中叫做keyPath,后面两个参数分别是改变前的值和改变后的值。
之后我们像runtime那样为我们的类派生出子类以便修改他的set方法。
创建类:Test_Obj_Notifying。
#import "Test_Obj.h"
//继承自我们的测试类
@interface Test_Obj_Notifying : Test_Obj
@end
#import "Test_Obj_Notifying.h"
@implementation Test_Obj_Notifying
//重写set方法。
- (void)setStr:(NSString *)str{
//取到旧值和新值之后要传给观察者
NSString *oldStr = self.str;
NSString *newStr = str;
//遍历那些观察的者来通知他们自己将要发生了变化。
for(id observer in ?????){
//获取属性名,使用KVO的时候那个KeyPath就是属性名,重写set方法的时候就用keypath,是从外面传进来的。这里就是@"str"。
[observer property:@"str" valueWillChange:oldStr newValue:newStr];
}
//我们自己的类可能重写过set方法,所以这里使用super调用我们的set方法。如果直接修改值可能会导致在原有类set方法中的某些操作被忽略。
[super setStr:str];
//遍历那些观察者来通知他们自己将要发生变化。
for(id observer in ?????){
[observer property:@"str" valueDidChange:oldStr newValue:newStr];
}
}
- (void)setNum:(NSInteger)num{
NSInteger oldNum = self.num;
NSInteger newNum = num;
for (id observer in ?????) {
[observer property:@"num" valueWillChange:@(oldNum) newValue:@(newNum)];
}
[super setNum:num];
for(id observer in ?????){
[observer property:@"num" valueDidChange:@(oldNum) newValue:@(newNum)];
}
}
@end
- 发现在?????处不知道有哪些观察者观察了我们,因此不能完成这些通知,因为KVO需要对所有NSObject都适用,因此我们不能重新规定一个XX_Object继承自NSObject,来给他添加NSMutableArray属性以储存哪些观察者,所以只能用Categary配合runtime绑定对象的方法来为一些需要被观察的对象绑定这些观察者的数组(在一些扩大按钮响应面积的方法中使用了类似的做法,他们为按钮绑定了4条@(float)类型的对象,重写判断点击事件是否在响应区域内的函数,这些函数调用categary提供的附加函数获取到绑定属性从而使按钮可以在更大的范围内响应点击,而不需要让所有按钮都继承自一个UIButton的子类)。
- 通过以上思路,我们可以如下解决观察者的保存问题:
- 创建categary文件:NSObject+NSOject_LWNotifying(这个命名很优秀)
#import
#import "LWNotifyingProtocol.h"
//规定数组内装的Obj类型,他们一定得实现了观察者的协议才能作为观察者。
@interface NSObject (NSObject_LWNotifying)
//为属性添加观察者
- (void)LW_addObserver:(id)observer forProperty:(NSString *)propertyName;
//获取某属性当前有哪些观察者
-(NSMutableArray>*)LW_getObserverMArrForProperty:(NSString *)propertyName;
@end
#import "NSObject+NSObject_LWNotifying.h"
#import
@implementation NSObject (NSObject_LWNotifying)
//懒加载 属性-观察者列表 的字典
- (NSMutableDictionary*)getPropertyMDic{
if (!objc_getAssociatedObject(self, @"propertyMDic")) {
NSMutableDictionary *mDic = [[NSMutableDictionary alloc] init];
objc_setAssociatedObject(self, @"propertyMDic", mDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return objc_getAssociatedObject(self, @"propertyMDic");
}
//懒加载某个属性的观察者列表
- (NSMutableArray>*)getObserverMArrForProperty:(NSString *)propertyName{
if (![[self getPropertyMDic] objectForKey:propertyName]) {
NSMutableArray *mArr = [[NSMutableArray alloc] init];
[[self getPropertyMDic] setObject:mArr forKey:propertyName];
}
return [[self getPropertyMDic] objectForKey:propertyName];
}
//这个和上面那个函数一样,但因为要暴露出去所以在名字上有所修改以和原生的KVO区分。
- (NSMutableArray>*)LW_getObserverMArrForProperty:(NSString *)propertyName{
return [self getObserverMArrForProperty:propertyName];
}
//为某一个属性加一个观察者
- (void)LW_addObserver:(id)observer forProperty:(NSString *)propertyName{
[[self LW_getObserverMArrForProperty:propertyName] addObject:observer];
}
@end
- 至此,我们这个类别就可以用来绑定观察者数组了。
- 将之前没有完成的Notifying类引入类别并修改为如下:
#import "Test_Obj.h"
#import "NSObject+NSObject_LWNotifying.h"
@interface Test_Obj_Notifying : Test_Obj
@end
#import "Test_Obj_Notifying.h"
@implementation Test_Obj_Notifying
- (void)setStr:(NSString *)str{
NSString *oldStr = self.str;
NSString *newStr = str;
//获取属性名,使用KVO的时候那个KeyPath就是属性名,重写set方法的时候就用keypath,是从外面传进来的。这里直接传进来@"str"。
for(id observer in [self LW_getObserverMArrForProperty:@"str"]){
[observer property:@"str" valueWillChange:oldStr newValue:newStr];
}
[super setStr:str];
for(id observer in [self LW_getObserverMArrForProperty:@"str"]){
[observer property:@"str" valueDidChange:oldStr newValue:newStr];
}
}
- (void)setNum:(NSInteger)num{
NSInteger oldNum = self.num;
NSInteger newNum = num;
for (id observer in [self LW_getObserverMArrForProperty:@"num"]) {
[observer property:@"num" valueWillChange:@(oldNum) newValue:@(newNum)];
}
[super setNum:num];
for(id observer in [self LW_getObserverMArrForProperty:@"str"]){
[observer property:@"num" valueDidChange:@(oldNum) newValue:@(newNum)];
}
}
- 至此,我么的KVO已完成一半,还需要提供观察者移除的方法,以供dealloc方法中调用,只需要从MArr中移除对应的id就可以了。
//遍历所有属性名都把该observer移除
- (void)LW_removeObserver:(id)observer{
for (NSString *propertyName in [[self getPropertyMDic] allKeys]) {
[[self getObserverMArrForProperty:propertyName] removeObject:observer];
}
}
- 接下来,就可以在实例视图中进行测试。
#import "ViewController.h"
#import "Test_Obj.h"//我们自己调用的时候是调用的这个类型的实例
#import "Test_Obj_Notifying.h"//但事实上,runtime偷偷改成调用了这个子类
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Test_Obj_Notifying *notifiedObj = [[Test_Obj_Notifying alloc] init];//创建一个子类实例
[notifiedObj LW_addObserver:self forProperty:@"str"];//为这个子类实例添加一个观察者self,也就是该视图控制器。观察str属性。
[notifiedObj LW_addObserver:self forProperty:@"num"];//同样添加自己为观察者,贯彻num属性。
notifiedObj.str = @"liwei";//修改str的值
notifiedObj.str = @"liwei1";//修改str的值
notifiedObj.num = 10;//修改num的值
notifiedObj.num = 100;//修改num的值
[notifiedObj changeToZero];//通过非set方法修改属性不会触发KVO。
[notifiedObj LW_removeObserver:self];//不要忘记移除自己,一般情况下在dealloc中移除,此处因Obj是临时创建的,直接移除。
}
//实现KVO的代理
- (void)property:(NSString *)propertyName valueWillChange:(id)oldValue newValue:(id)newValue{
if ([propertyName isEqualToString:@"str"]) {
NSLog(@"Will方法中");
NSLog(@"str旧值:%@",oldValue);
NSLog(@"str新值:%@",newValue);
}else if([propertyName isEqualToString:@"num"]){
NSLog(@"Will方法中");
NSLog(@"num旧值:%@",oldValue);
NSLog(@"num新值:%@",newValue);
}else{
NSLog(@"其他属性");
}
}
//实现KVO的代理
- (void)property:(NSString *)propertyName valueDidChange:(id)oldValue newValue:(id)newValue{
if ([propertyName isEqualToString:@"str"]) {
NSLog(@"Did方法中");
NSLog(@"str旧值:%@",oldValue);
NSLog(@"str新值:%@",newValue);
}else if([propertyName isEqualToString:@"num"]){
NSLog(@"Did方法中");
NSLog(@"num旧值:%@",oldValue);
NSLog(@"num新值:%@",newValue);
}else{
NSLog(@"其他属性");
}
}
@end
- 至此,一个完成KVO机制就实现完了。实际编码中,以上的过程全部由runtime实现,我们只需要在需要的viewController或者其他环境中补充好回调函数就可以接收属性变化。只需要写最后一段包括的代码。
- 总结一下思路:通过派生子类继承原类重写set方法,在set方法中调用观察者的回调函数,思路很容易,但由于实际实现的时候要做到通用,因此必须对NSObject拓展,而不能修改NSObject原有的定义,因此要用到类别;存储观察者的数组需要在类别中动态绑定到该实例上(categary不能拓展属性,但可以拓展出get方法和set方法假装拓展属性,本质是使用objc_AssXXX绑定对象)。