自己实现kvo之前,需要知道iOS系统对kvo的实现。
系统实现kvo的原理
这依赖了OC强大的runtime特性。
在我们对某个Student的实例对象的name属性addObserver时,系统会动态创建一个继承自Student的类(NSKVONotifying_Student
),并重写setName:
方法。在这里获取新值、旧值、调用父类方法、并发通知给监听者。把对象的isa指针(指向该对象所属的类对象
)指向该子类。
同时,系统还重写了class方法,使该方法返回父类名称,以使开发者不会察觉。
我们通过断点调试,验证下:
- (void)viewDidLoad {
[super viewDidLoad];
_s = [[Student alloc] init];
[_s addObserver:self forKeyPath:@"score" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
在addObserver处打断点。
可以看到s的isa指针指向
Student
类。单步执行下:
isa指针指向了NSKVONotifying_Student
类。
自定义kvo
既然所有对象都能使用kvo的方法,那先建个NSObject的category。并添加addObserver方法:
// NSObject+Extension.h
#import <Foundation/Foundation.h>
@interface NSObject (Extension)
- (void)ws_addObserver:(NSObject *_Nonnull)observer forKeyPath:(NSString *_Nonnull)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
就是系统方法加了前缀。
仿照系统的方式进行实现。
- 动态创建子类
- 重写被监听属性的set方法
- 改变对象的isa指针,使其指向子类
接下来 一步步实现:
1.动态创建子类(WSKVONotifying_原类名)
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"WSKVONotifying_%@", oldClassName];
Class class = objc_getClass(newClassName.UTF8String);
if (!class) {
class = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
objc_registerClassPair(class);
}
2.重写被监听属性的set方法
- 首先动态添加set方法,参数
keypath
就是get方法,通过它拼接处set方法(就是首字母大写,加:)。
//set方法首字母大写
NSString *keyPathChange = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
NSString *setNameStr = [NSString stringWithFormat:@"set%@", keyPathChange];
SEL setSEL = NSSelectorFromString([setNameStr stringByAppendingString:@":"]);
//添加set方法
Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
const char *types = method_getTypeEncoding(getMethod);
class_addMethod(class, setSEL, (IMP)setMethod, types);
- set方法的实现:包括调用父类的set方法,获取旧值、新值,获取observer并通知observer。
void setMethod(id self, SEL _cmd, id newValue) {
//获取get、set方法名
NSString *setNameStr = objc_getAssociatedObject(self, WSKVO_setter);
NSString *getNameStr = objc_getAssociatedObject(self, WSKVO_getter);
//保存子类类型
Class class = [self class];
//isa指向原类
object_setClass(self, class_getSuperclass(class));
//调用原类get方法,获取oldValue
id oldValue = objc_msgSend(self, NSSelectorFromString(getNameStr));
//调用原类set方法
objc_msgSend(self, NSSelectorFromString([setNameStr stringByAppendingString:@":"]), newValue);
//调用observer的observeValueForKeyPath: ofObject: change: context:方法
id observer = objc_getAssociatedObject(self, WSKVO_observer);
NSMutableDictionary *change = @{}.mutableCopy;
if (newValue) {
change[NSKeyValueChangeNewKey] = newValue;
}
if (oldValue) {
change[NSKeyValueChangeOldKey] = oldValue;
}
objc_msgSend(observer, @selector(observeValueForKeyPath: ofObject: change: context:), getNameStr, self, change, nil);
//isa改回子类类型
object_setClass(self, class);
}
3.改变isa指针的指向
object_setClass(self, class);
上面是分段写的,接下来附上完整的.m实现:
#import "NSObject+Extension.h"
#import <objc/message.h>
static const char *WSKVO_observer = "WSKVO_observer";
static const char *WSKVO_getter = "WSKVO_getter";
static const char *WSKVO_setter = "WSKVO_setter";
@implementation NSObject (Extension)
- (void)ws_addObserver:(NSObject *_Nonnull)observer forKeyPath:(NSString *_Nonnull)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
//创建、注册子类
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"WSKVONotifying_%@", oldClassName];
Class class = objc_getClass(newClassName.UTF8String);
if (!class) {
class = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
objc_registerClassPair(class);
}
//set方法首字母大写
NSString *keyPathChange = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
NSString *setNameStr = [NSString stringWithFormat:@"set%@", keyPathChange];
SEL setSEL = NSSelectorFromString([setNameStr stringByAppendingString:@":"]);
//添加set方法
Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
const char *types = method_getTypeEncoding(getMethod);
class_addMethod(class, setSEL, (IMP)setMethod, types);
//改变isa指针,指向子类
object_setClass(self, class);
//保存observer
objc_setAssociatedObject(self, WSKVO_observer, observer, OBJC_ASSOCIATION_ASSIGN);
//保存set、get方法名
objc_setAssociatedObject(self, WSKVO_setter, setNameStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, WSKVO_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
void setMethod(id self, SEL _cmd, id newValue) {
//获取get、set方法名
NSString *setNameStr = objc_getAssociatedObject(self, WSKVO_setter);
NSString *getNameStr = objc_getAssociatedObject(self, WSKVO_getter);
//保存子类类型
Class class = [self class];
//isa指向原类
object_setClass(self, class_getSuperclass(class));
//调用原类get方法,获取oldValue
id oldValue = objc_msgSend(self, NSSelectorFromString(getNameStr));
//调用原类set方法
objc_msgSend(self, NSSelectorFromString([setNameStr stringByAppendingString:@":"]), newValue);
//调用observer的observeValueForKeyPath: ofObject: change: context:方法
id observer = objc_getAssociatedObject(self, WSKVO_observer);
NSMutableDictionary *change = @{}.mutableCopy;
if (newValue) {
change[NSKeyValueChangeNewKey] = newValue;
}
if (oldValue) {
change[NSKeyValueChangeOldKey] = oldValue;
}
objc_msgSend(observer, @selector(observeValueForKeyPath: ofObject: change: context:), getNameStr, self, change, nil);
//isa改回子类类型
object_setClass(self, class);
}
@end
测试下
测试代码如下:可以正常实现监听
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor purpleColor];
_s = [[Student alloc] init];
//使用自定义方法实现kvo监听
[_s ws_addObserver:self forKeyPath:@"score" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"old=%@ new=%@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static NSInteger num = 0;
_s.score = @(num++);
}
update 2017.10.26
为kvo添加block回调
大部分代码没有变,只是添加了block回调。
// NSObject+WSKVO.h
#import <Foundation/Foundation.h>
typedef void(^WSKVOBlock)(NSDictionary * _Nonnull change);
@interface NSObject (WSKVO)
- (void)ws_observerkeyPath:(NSString *_Nonnull)keyPath options:(NSKeyValueObservingOptions)options block:(WSKVOBlock _Nonnull )block;
@end
在方法实现中,使用关联对象保存block,在动态添加的setter方法中取出并执行block。
NSObject+WSKVO.m
#import "NSObject+WSKVO.h"
#import <objc/message.h>
static const char *WSKVO_getter = "WSKVO_getter";
static const char *WSKVO_setter = "WSKVO_setter";
static const char *WSKVO_block = "WSKVO_block";
@implementation NSObject (WSKVO)
- (void)ws_observerkeyPath:(NSString *_Nonnull)keyPath options:(NSKeyValueObservingOptions)options block:(WSKVOBlock _Nonnull )block {
//创建、注册子类
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"WSKVONotifying_%@", oldClassName];
Class class = objc_getClass(newClassName.UTF8String);
if (!class) {
class = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
objc_registerClassPair(class);
}
//set方法首字母大写
NSString *keyPathChange = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
NSString *setNameStr = [NSString stringWithFormat:@"set%@", keyPathChange];
SEL setSEL = NSSelectorFromString([setNameStr stringByAppendingString:@":"]);
//添加set方法
Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
const char *types = method_getTypeEncoding(getMethod);
class_addMethod(class, setSEL, (IMP)setMethod, types);
//改变isa指针,指向子类
object_setClass(self, class);
//保存set、get方法名
objc_setAssociatedObject(self, WSKVO_setter, setNameStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, WSKVO_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
//保存block
objc_setAssociatedObject(self, WSKVO_block, block, OBJC_ASSOCIATION_COPY);
}
void setMethod(id self, SEL _cmd, id newValue) {
//获取get、set方法名
NSString *setNameStr = objc_getAssociatedObject(self, WSKVO_setter);
NSString *getNameStr = objc_getAssociatedObject(self, WSKVO_getter);
//保存子类类型
Class class = [self class];
//isa指向原类
object_setClass(self, class_getSuperclass(class));
//调用原类get方法,获取oldValue
id oldValue = objc_msgSend(self, NSSelectorFromString(getNameStr));
//调用原类set方法
objc_msgSend(self, NSSelectorFromString([setNameStr stringByAppendingString:@":"]), newValue);
NSMutableDictionary *change = @{}.mutableCopy;
if (newValue) {
change[NSKeyValueChangeNewKey] = newValue;
}
if (oldValue) {
change[NSKeyValueChangeOldKey] = oldValue;
}
//取出block
WSKVOBlock block = objc_getAssociatedObject(self, WSKVO_block);
if (block) {
block(change);
}
//isa改回子类类型
object_setClass(self, class);
}
@end
使用block注意避免循环引用问题。
demo已更新,为方便查看,两种方式写在了两个分类中。Demo地址