首先了解一下系统的KVO实现原理:其实就是动态的创建了一个被观察者的子类,然后动态修改它的isa指针指向它的子类,在子类里重写属性的set方法,最后在set方法里监听属性变化,并发出通知。
1、验证系统原理:
打个断点,发现实例s的isa是指向Student的,然后单步执行一下,
这时候发现实例s的isa指针变成了
NSKVONotifying_Student
,由此可见,当我们添加观察者的时候,系统动态的创建了一个子类NSKVONotifying_Student
,并把s的类型修改成了它。
2、接下来我们自己用RunTime来仿照系统KVO原理来自己写一个KVO
首先创建一个NSObject的分类NSObject+KVO
,然后自己实现一个监听方法。
#import "NSObject+KVO.h"
#import
static const char* SJKVOAssiociateKey = "SJKVOAssiociateKey";
@implementation NSObject (KVO)
-(void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
Class newClass = [self createClass:keyPath];
object_setClass(self, newClass);
// 4.将观察者与对象绑定
objc_setAssociatedObject(self, SJKVOAssiociateKey, observer, OBJC_ASSOCIATION_ASSIGN);
}
- (Class) createClass:(NSString*) keyPath {
// 1. 拼接子类名 / SJKVO_Student
NSString* oldName = NSStringFromClass([self class]);
NSString* newName = [NSString stringWithFormat:@"SJKVO_%@", oldName];
// 2. 创建并注册类
Class newClass = NSClassFromString(newName);
if (!newClass) {
// 创建并注册类
newClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
objc_registerClassPair(newClass);
// 动态添加方法
// class
Method classMethod = class_getInstanceMethod([self class], @selector(class));
const char* classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, @selector(class), (IMP)SJ_class, classTypes);
}
// setter
NSString* setterMethodName = getSetter(keyPath);
SEL setterSEL = NSSelectorFromString(setterMethodName);
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char* setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)SJ_setKey, setterTypes);
return newClass;
}
Class SJ_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
void SJ_setKey(id self, SEL _cmd, id newValue) {
struct objc_super oldSuper = {self,class_getSuperclass([self class])};
// 修改属性值
objc_msgSendSuper(&oldSuper, _cmd, newValue);
// 拿出观察者
id observer = objc_getAssociatedObject(self, SJKVOAssiociateKey);
NSLog(@"---%@",newValue);
// 调用observer
NSString *methodName = NSStringFromSelector(_cmd);
NSString *key = getValueKey(methodName);
objc_msgSend(observer, sel_registerName("observeValueForKeyPath:ofObject:change:context:"),key,self,@{key:newValue},nil);
// [observer observeValueForKeyPath:key ofObject:self change:@{key:newValue} context:nil];
}
// key -> setter
static NSString * getSetter(NSString *keyPath){
if (keyPath.length <= 0) { return nil; }
NSString *firstString = [[keyPath substringToIndex:1] uppercaseString];
NSString *leaveString = [keyPath substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
// cmd -> key
NSString* getKey(NSString * cmd) {
if (cmd.length <= 0 || ![cmd hasPrefix:@"set"] || ![cmd hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, cmd.length-4);
NSString *getter = [cmd substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
getter = [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
return getter;
}
@end
然后在用我们自己的方法来添加一下观察者,看是否能观察到
- (void)viewDidLoad {
[super viewDidLoad];
Student *s = [[Student alloc] init];
// [s addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
[s SJ_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
self.s = s;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"+++%@",self.s.age);
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
i++;
self.s.age = [NSString stringWithFormat:@"%ld",i];
}
通过打印可以看出跟系统的效果一样。
基本都有注释,我就不详解了,里边一些关于runtime的动态函数和消息转发函数,在我之前关于runtime的文章里都有详解,不了解可以参照前几篇文章做基础。
3、用block回调
用系统的回调会一个问题,就是如果观察的属性多了,在回调方法里需要先判断是哪个对象的哪个属性,比较麻烦,但是用block回调的话,就省去了这些麻烦,并且代码逻辑更清晰,更紧凑。接下来代码多了,我们顺便整理封装一下。
首先定义一个block和一个可以有block的构造方法:
typedef void(^ValueChangeBlock)(id observer, NSString* keyPath, id oldValue, id newValue);
@interface NSObject (KVO)
// 系统回调
- (void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
// block回调
- (void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context ValueChangeBlock:(ValueChangeBlock)valueChangeBlock;
@end
然后在.m文件内部创建一个类,来存储要监听的信息:
static const char* SJKVOAssiociateKey = "SJKVOAssiociateKey";
@interface SJInfo : NSObject
@property (nonatomic, weak) NSObject* observer;
@property (nonatomic, strong) NSString* keyPath;
@property (nonatomic, copy) ValueChangeBlock valueChangeBlock;
@end
@implementation SJInfo
- (instancetype) initWithObserver:(NSObject*)observer forKeyPath:(NSString*) keyPath valueChangeBlock:(ValueChangeBlock) block {
if (self == [super init]) {
_observer = observer;
_keyPath = keyPath;
_valueChangeBlock = block;
}
return self;
}
@end
然后在创建是就把参数和block保存到数组里,以便下边拿出来调用:
-(void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context ValueChangeBlock:(ValueChangeBlock)valueChangeBlock {
Class newClass = [self createClass:keyPath];
object_setClass(self, newClass);
// 信息保存
SJInfo* info = [[SJInfo alloc] initWithObserver:observer forKeyPath:keyPath valueChangeBlock:valueChangeBlock];
NSMutableArray* array = objc_getAssociatedObject(self, SJKVOAssiociateKey);
if (!array) {
array = [NSMutableArray array];
objc_setAssociatedObject(self, SJKVOAssiociateKey, array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[array addObject:info];
}
void SJ_setKey(id self, SEL _cmd, id newValue) {
struct objc_super oldSuper = {self,class_getSuperclass(object_getClass(self))};
// 获取key
NSString *key = getKey(NSStringFromSelector(_cmd));
// 获取旧值
id oldValue = objc_msgSendSuper(&oldSuper, NSSelectorFromString(key));
// 修改属性值
objc_msgSendSuper(&oldSuper, _cmd, newValue);
NSMutableArray* array = objc_getAssociatedObject(self, SJKVOAssiociateKey);
if (array) {
for (SJInfo* info in array) {
if ([info.keyPath isEqualToString:key]) {
info.valueChangeBlock(info.observer, key, oldValue, newValue);
return;
}
}
}
}
在setKey方法里拿出数组中的信息,找到对应的key,然后block回调就可以了,这样就可以同时监听多个属性了。
销毁观察者
其实销毁观察者就是把isa指针指从子类回到原来就可以了,我们把他放在dealloc方法里来做比较合适,有两种方法:1.利用hook在创建子类的方法里做方法交换,把dealloc的方法实现指向自己的方法里,然后做isa指回;2.也是在创建子类的时候同时动态添加自己的dealloc方法来做,我们这里就用第二种实现一下:
放在子类的创建方法里,保证只创建一次
// 添加SJ_Dealloc,销毁观察者
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char* deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)SJ_Dealloc, deallocTypes);
void SJ_Dealloc(id self, SEL _cmd) {
// 父类
Class superClass = [self class];//class_getSuperclass(object_getClass(self));
object_setClass(self, superClass);
}
最后在外边调用一下,非常方便
[s SJ_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil ValueChangeBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
NSLog(@"oldValue ---- %@, newValue ---- %@", oldValue, newValue);
}];
[s SJ_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil ValueChangeBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
NSLog(@"oldValue ++++ %@, newValue ++++ %@", oldValue, newValue);
}];