1 KVO的原理
当一个类的属性被观察的时候,系统会通过runtime动态的创建一个该类的派生类,并且会在这个类中重写基类被观察的属性的setter方法,而且系统将这个对象的isa指针指向了派生类,从而实现了给监听的属性赋值时调用的是派生类的setter方法。重写的setter方法会在调用原setter方法前后,通知观察对象值得改变。此外,派生类还重写了 dealloc 方法来释放资源。
2动手实现一个KVO
假设有一个Person类
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *age;
@end
@implementation Person
@end
创建一个NSObject+MyKVO的分类,然后再.m文件中加入以下代码
- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
//给新类取名字
NSString *currentClassName = NSStringFromClass([self class]);
NSString *createdClassName = [@"MyKVO" stringByAppendingString:currentClassName];
const char * newname = [createdClassName UTF8String];
//用runtime创建一个类,创建完类要注册才能用。第一个参数表示新类继承谁
Class newClass = objc_allocateClassPair([self class], newname, 0);
objc_registerClassPair(newClass);
//添加一个name的set方法
class_addMethod(newClass, @selector(setName:), (IMP)setName, "v@:@");
//设置一个对象的类(即改编isa指针),也就是说当前这个对象现在是新类的实例
object_setClass(self, newClass);
const void *key = "qwer";
//添加一个成员变量名字叫objc,值就是观察者observer。
objc_setAssociatedObject(self, key, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//我们要在setName调用super 的set 方法还要通知外界
void setName(id self, SEL _cmd, NSString * newName){
//保存新类
id class = [self class];
//先把isa指针指向它的父类(就是原来那个类)
object_setClass(self, class_getSuperclass(class));
//让父类调用setName方法
objc_msgSend(self, @selector(setName:),newName);
//拿到observer
id objc = objc_getAssociatedObject(self, "qwer");
//调用observer的observeValueForKeyPath方法,参数1:person本身,2要监听的name
objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:),self,@"name",nil,nil);
//改回新类类型
object_setClass(self, class);
}
此时在VC中就能监听name的改变了
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc]init];
[self.person my_addObserver:self forKeyPath:@"name" options:0 context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到了%@",_person.age);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
static int a = 1;
a++;
self.person.name = [NSString stringWithFormat:@"%d",a];
}
3 完善
这个时候MyKVO只能监听name属性,还不能监听其他属性,比如不能监听age,下面修改完善NSObject+MyKVO
//转换被监听的keyPath为SEL
-(SEL)convertKeyPathToSEL:(NSString*)keyPath{
NSString *firstUp = [[keyPath uppercaseString]substringToIndex:1];
NSString *otherStr = [keyPath substringFromIndex:1];
NSString *SELString = [[@"set" stringByAppendingString:[firstUp stringByAppendingString:otherStr]]stringByAppendingString:@":"];
return NSSelectorFromString(SELString);
}
- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
NSString *currentClassName = NSStringFromClass([self class]);
NSString *createdClassName = [@"MyKVO" stringByAppendingString:currentClassName];
const char * newname = [createdClassName UTF8String];
Class newClass = objc_allocateClassPair([self class], newname, 0);
objc_registerClassPair(newClass);
//添加一个被监听属性的set方法,IMP是setName
class_addMethod(newClass,[self convertKeyPathToSEL:keyPath], (IMP)setName, "v@:@");
object_setClass(self, newClass);
const void *key = "myObserver";
const void *mykeyPath = "myKeyPath";
objc_setAssociatedObject(self, key, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, mykeyPath, keyPath, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//我们要在setName调用super 的set 方法还要通知外界
void setName(id self, SEL _cmd, NSString * newName){
NSString *keyPath = objc_getAssociatedObject(self, "myKeyPath");
id class = [self class];
object_setClass(self, class_getSuperclass(class));
//让父类调用set方法
objc_msgSend(self, [self convertKeyPathToSEL:keyPath],newName);
id objc = objc_getAssociatedObject(self, "myObserver");
objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,nil,nil);
object_setClass(self, class);
}