KVO 是 Objective-C 对观察者模式(Observe Pattern)的实现,也是Cocoa Binding 的基础。当某个属性发生更改时,观察者对象会获得通知。使用方便,你不需要给被观察者对象添加任何额外的代码就能实现。这是怎么做到的呢?
一、KVO的实现原理
新建一个 Person_KVO 类,继承自NSObject,并添加一个属性 name,做如下处理:
Person_KVO.h
@interface Person_KVO : NSObject
@property(nonatomic,strong)NSString * name;
@end
*****************************************************************************
KVOController.m
@interface KVOController ()
@property(nonatomic,strong) Person_KVO *p;
@end
@implementation KVOController
- (void)viewDidLoad {
[super viewDidLoad];
Person_KVO *p = [[Person_KVO alloc]init];// A
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];// B
_p = p;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"观察到了 %@ 的 %@ 属性变化为了:%@",NSStringFromClass([object class]),keyPath,change[@"new"]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
static int a = 0;
a++;
self.p.name = [NSString stringWithFormat:@"Tom-%d",a];
}
@end
此时,如果我们点击屏幕就会得如下输出结果:
接下来我们在 viewDidLoad 方法中打个断点,重新运行一下程序,当执行完 A 行代码时看下对象 p 的情况: 当执行完 B 行代码后,再看下 p 的情况: 可以发现,p 对象在添加观察者之后类型由原来的 Person_KVO 变成了 NSKVONotifying_Person_KVO 类型。什么原因呢?
一个对象在调用 addObserver 方法时,会动态创建一个带有NSKVONotifying_ 前缀的原类的子类,用来重写所有的 setter 方法,并且子类的 - (Class)class 和 -(Class)superClass 方法会被重写,返回子类(带前缀的类)的类型和父类(原始类)的类型,最后将对象的类型改为这个带前缀的子类的类型。
因此,当我们调用 p 的 setName: 方法时,实际上调用的是在 NSKVONotifying_Person_KVO 中重写的:
@implementation NSKVONotyfing_Person_KVO
- (void)setName:(NSString *)name
{
[self willChangeValueForKey:@"name"];
[super setName:name];//设置新的属性值
[self didChangeValueForKey:@"name"];//通知观察者值的变化
}
二、利用 Runtime 实现上述案例
新建一个关于 NSObejct 的类别:
#import
@interface NSObject (LFKVO)
- (void)LF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
#import "NSObject+LFKVO.h"
#import
@implementation NSObject (LFKVO)
- (void)LF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
/*
1.动态创建一个类
2.改变方法调用者的类型!!self
*/
//1.动态创建一个类.(1)继承谁 (2)类名字
NSString *oldName = NSStringFromClass([self class]);
NSString *newName = [@"LFKVO_" stringByAppendingString:oldName];
Class NewClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
//注册该类
objc_registerClassPair(NewClass);
//2.改变类的类型
object_setClass(self, NewClass);
//3.添加setName方法(子类里没有该方法),重写父类方法就是添加一个一模一样的方法
class_addMethod(NewClass, @selector(setName:), (IMP)setName,"V@:@");
//4.将观察者绑定为属性
objc_setAssociatedObject(self, @"LF", observer, OBJC_ASSOCIATION_ASSIGN);
}
void setName(id self,SEL _cmd,id newName)
{
NSLog(@"新来的 name 是:%@",newName);
id class = [self class];
//调用一下父类的setName方法
object_setClass(self, class_getSuperclass([self class]));//改变类型为父类类型
objc_msgSend(self, _cmd,newName);
//通知一下外界
id observer = objc_getAssociatedObject(self, @"LF");
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"new":newName},nil);
//改回原类型
object_setClass(self, class);
}
@end
*****************************************************************
#import "KVOController.h"
#import "Person_KVO.h"
#import "NSObject+LFKVO.h"
@interface KVOController ()
@property(nonatomic,strong) Person_KVO *p;
@end
@implementation KVOController
- (void)viewDidLoad {
[super viewDidLoad];
Person_KVO *p = [[Person_KVO alloc]init];
[p LF_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
_p = p;
}
运行后点击屏幕,输出结果如下
三、优化上述案例
上面的写法是为了帮助大家理解 KVO 底层的实现方式,但如果 Person_KVO 类新增加了一个属性 hobby,能否也能用于监听该 hobby属性呢?
#import
@interface Person_KVO : NSObject
@property(nonatomic,strong)NSString * name;
@property(nonatomic,strong)NSString * hobby;
@end
******************************************************************
#import "KVOController.h"
#import "Person_KVO.h"
#import "NSObject+LFKVO.h"
@interface KVOController ()
@property(nonatomic,strong) Person_KVO *p;
@end
@implementation KVOController
- (void)viewDidLoad {
[super viewDidLoad];
Person_KVO *p = [[Person_KVO alloc]init];
[p LF_addObserver:self forKeyPath:@"hobby" options:NSKeyValueObservingOptionNew context:nil];
_p = p;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"观察到了 %@ 的 %@ 属性变化为了:%@",NSStringFromClass([object class]),keyPath,change[@"new"]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
static int b = 10;
b++;
self.p.hobby = [NSString stringWithFormat:@"dance-%003d",b];
}
运行后,点击屏幕输出结果如下:
没有任何输出。仔细看下上面 Runtime 的实现,细心的小伙伴会发现这个只是针对 name 属性的监听实现。想要监听所有的属性,需要按如下方案优化:
#import "NSObject+LF_KVO.h"
#import
NSString *const kLFKVOClassPrefix = @"LFKVO_";
NSString *const kLFKVOObserverKey = @"LFKVOObserverKey";
@implementation NSObject (LF_KVO)
- (void)LF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
//1.动态创建一个类 (1)继承谁 (2)类名字
NSString * oldClassName = NSStringFromClass([self class]);
NSString * newClassName = [[NSString alloc]initWithString:oldClassName];
if(![oldClassName hasPrefix:kLFKVOClassPrefix])
{
newClassName = [kLFKVOClassPrefix stringByAppendingString:oldClassName];
Class NewClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//注册该类
objc_registerClassPair(NewClass);
//2.改变类的类型
object_setClass(self, NewClass);
}
//3.添加 set 方法(子类在底层没有该方法),重写父类方法就是添加一个一模一样的方法
SEL setSelector = [self setSelectorFromString:keyPath];
Method setterMethod = class_getInstanceMethod([self superclass], setSelector);
if(!setterMethod)
{
NSString *reason =[NSString stringWithFormat:@"%@ 类没有关于 %@ 的 set 方法",NSStringFromClass([self superclass]),keyPath];
NSLog(@"%@",reason);
return;
}
class_addMethod([self class], setSelector, (IMP)kvo_setter, method_getTypeEncoding(setterMethod));
//4.绑定观察者为属性
objc_setAssociatedObject(self, (__bridge const void*)kLFKVOObserverKey, observer, OBJC_ASSOCIATION_ASSIGN);
}
static void kvo_setter(id self,SEL _cmd,id newValue)
{
NSString * key = [self getterNameFromSetSelector:_cmd];
NSLog(@"新来的 key 是:%@",key);
NSLog(@"新来的 value 是:%@",newValue);
Class class = object_getClass(self);
object_setClass(self,class_getSuperclass([self class]));//改变类型为父类类型
//改变属性值
objc_msgSend(self, _cmd,newValue);
//通知外界
id observer = objc_getAssociatedObject(self, (__bridge const void*)kLFKVOObserverKey);
objc_msgSend(observer,
@selector(observeValueForKeyPath:ofObject:change:context:),
key,self,@{@"new":newValue},nil);
object_setClass(self,class);//改回原类型
}
#pragma mark--根据key值获取对应的set方法
- (SEL)setSelectorFromString:(NSString *)key{
if(!key.length)
{
return nil;
}
NSString * prefixStr = [[key substringToIndex:1] uppercaseString];
NSString * secondStr = [key substringFromIndex:1];
return NSSelectorFromString([NSString stringWithFormat:@"set%@%@:",prefixStr,secondStr]);
}
#pragma mark-- 根据set方法获取对应的key值
- (NSString *)getterNameFromSetSelector:(SEL)selector
{
NSString * setterName = NSStringFromSelector(selector);
if(!setterName.length)
{
return nil;
}
NSString * getterPrefix = [[setterName substringWithRange:NSMakeRange(3, 1)]lowercaseString];
NSString *getterSecond = [setterName substringWithRange:NSMakeRange(4, setterName.length - 5)];
return [NSString stringWithFormat:@"%@%@",getterPrefix,getterSecond];
}
@end
********************************************************
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person_KVO * p = [[Person_KVO alloc]init];
[p LF_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
[p LF_addObserver:self forKeyPath:@"hobby" options:NSKeyValueObservingOptionNew context:nil];
_p = p;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"观察到了 %@ 的 %@ 属性变化为了:%@",NSStringFromClass([object class]),keyPath,change[@"new"]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
static int a = 0;
a++;
_p.name = [NSString stringWithFormat:@"Tome_%03d",a];
static int b = 10;
b++;
_p.hobby = [NSString stringWithFormat:@"thong_%03d",b];
}
运行后,点击屏幕结果如下:
四、优化系统KVO的使用方式
当我们所监听对象的属性改变后,会做如下处理
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:@"name"])
{
//处理
}
if([keyPath isEqualToString:@"hobby"])
{
//处理
}
......
}
如果监听的属性比较多的话,就需要在observeValueForKeyPath:方法中做多种情况处理,使用起来很不友好。我想到了如下方案:
#import
typedef void (^KVO_ObserverBlock)(NSString *keyPath,id object,NSDictionary *change);
@interface NSObject (LFKVO)
@property (nonatomic,strong)KVO_ObserverBlock kvo_observerBlock;
- (void)WLF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context block:(KVO_ObserverBlock)block;
@end
#import "NSObject+WLFKVO.h"
#import
@implementation NSObject (LFKVO)
static char kvo_key;
- (void)setKvo_observerBlock:(KVO_ObserverBlock)kvo_observerBlock
{
objc_setAssociatedObject(self, &kvo_key, kvo_observerBlock, OBJC_ASSOCIATION_COPY);
}
- (KVO_ObserverBlock)kvo_observerBlock
{
return objc_getAssociatedObject(self, &kvo_key);
}
- (void)LF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block:(KVO_ObserverBlock)block
{
self.kvo_observerBlock = block;
[self addObserver:observer forKeyPath:keyPath options:options context:context];
}
使用方式为:
@interface KVOController ()
@property(nonatomic,strong) Person_KVO *p;
@end
@implementation KVOController
- (void)viewDidLoad {
[super viewDidLoad];
Person_KVO *p = [[Person_KVO alloc]init];
[p LF_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil block:^(NSString *keyPath, id object, NSDictionary *change) {
NSLog(@"观察到了 %@ 的 %@ 属性变化为了:%@",NSStringFromClass([object class]),keyPath,change[@"new"]);
}];
//
[p LF_addObserver:self forKeyPath:@"hobby" options:NSKeyValueObservingOptionNew context:nil block:^(NSString *keyPath, id object, NSDictionary *change) {
NSLog(@"观察到了 %@ 的 %@ 属性变化为了:%@",NSStringFromClass([object class]),keyPath,change[@"new"]);
}];
_p = p;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
_p.kvo_observerBlock(keyPath, object, change);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
static int a = 0;
a++;
self.p.name = [NSString stringWithFormat:@"Tom-%d",a];
static int b = 10;
b++;
self.p.hobby = [NSString stringWithFormat:@"dance-%003d",b];
}
大家谁有更好的实现方式可以随时告诉我啊
五、总结
看到了几个较好的问题,以此来做个总结吧。
keyPath是什么
首先keyPath,是对于setter方法的关联,会使用keypath作为后缀去寻找原类的setter方法的方法签名,和实际存取对象和property名称没有关系。所以这也是为什么我们重命名了setter方法之后,没有办法再去使用KVO或KVC了,需要手动调用一次willChangeValue方法。
子类继承父类的一个属性,当这个属性被改变时,KVO能否观察到?
因为继承的关系Father <- Son <- KVOSon,当我监听一个父类属性的keyPath的时候,Son实例同样可以通过消息查找找到父类的setter方法,再将该方法加入到KVOSon类当中去。
子类继承父类的一个未暴露的属性,当这个属性被改变时,KVO能否观察到?
由于在overrideSetterFor中,我们是直接通过sel去得到方法签名signature,所以和暴不暴露没啥关系。
子类继承父类属性并重写了它的setter方法,当这个属性被改变时,KVO能否观察到?
在上一条中知道,其实子类监听父类属性,并不依赖继承,而是通过ISA指针在消息转发的时候能够获取到父类方法就足够。所以当我们重写父类setter方法,相当于在子类定义了该setter函数,在我们去用sel找方法签名时,直接在子类中就拿到了,甚至都不需要去到父类里。所以理解了KVO监听父类属性和继承没有直接联系这一点,就不再纠结set方法是否重写这个问题了。
欢迎大家多提宝贵意见~~