KVO概括
大家都知道kvo是一种设计模式,是一种键值观察,当属性的值改变时候会触发回调,获取该属性的旧值和新值。但是可能有些朋友不清楚什么时候用它,使用场景是什么。当需要监听一个属性的值改变时候我们可以用到它。比如:
- 当图片的url改变时候自动加载新的图片。
- 当scrollView的offset改变时得到回调获取offset的值,这时候不用delegate较好,特别是封装一个框架时候,如果用delegate那么框架的使用者也可能用它的delegate导致框架的delegate不会执行,但是kvo不会出现这种问题。
- 比如监听一段mp3声音进度的属性,根据改变的值来进行UI绘制。
- 比如监听一些开关量进行绘制UI等等。可见KVO还是很常用的,也很实用。
KVO的简单使用
- 监听Person的对象p1的name属性值的变化
//注册kvo[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];//属性赋值- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event{ [super touchesBegan:touches withEvent:event]; count++; p1.name = [NSString stringWithFormat:@"%d",count];}//释放- (void)dealloc{ [p1 removeObserver:self forKeyPath:@"name"];}复制代码
- 手动kvo,可以手动控制是否触发kvo的回调
//重写Person类的automaticallyNotifiesObserversForKey返回NO即关闭了自动kvo@implementation Person+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{ return NO;}@end//注册kvo[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];//属性值变化(其实只要该对象的成员变量的值改变即可)- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event{ [super touchesBegan:touches withEvent:event]; count++; [p1 willChangeValueForKey:@"name"]; p1.name = [NSString stringWithFormat:@"%d",count]; [p1 didChangeValueForKey:@"name"];}//kvo回调- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ }//释放- (void)dealloc{ [p1 removeObserver:self forKeyPath:@"name"];}复制代码
KVO的底层实现探索
系统的kvo是怎么实现的呢?为什么只要对象的属性变化了就会触发回调呢?我也很好奇,在查看资料之前,自己也考虑了一下怎么实现。
首先系统的kvo任意一个对象都可以调用addObserver方法,可以确定应该是NSObject的分类,并新增了这个addObserver的方法。接下来想到的是hook,比如hook Person类对应的setter方法。利用runTime方法交换实现在调用这个setter方法之前获取该属性对应的成员变量的值,得到旧值。之后再调用该setter方法之后再次获取该属性对应的成员变量的值,得到新值。最后再调用observeValueForKeyPath方法把新旧值传递给observe对应的类。接下来查看资料,Oh,My God 并不是自己想的那样,系统在调用addObserver方法时候动态的创建了一个新的子类继承该被监听的对象所对应的类。并重写了父类的setter方法。并把对象的isa指针从父类指向了该子类。这样当父类的对象调用setter方法时候就会调用子类的setter方法,在该setter方法内部调用了willChangeValueForKey,didChangeValueForKey方法。之后系统会调用observeValueForKeyPath方法,把旧的和新的值传递给oberver所对应的类。在新的子类里除了重写了父类的setter方法以外还重写了class方法,该方法是为了外界调用class时候隐藏新创建的子类。有一点很奇怪当我们在addObserver方法后打一个断点时并把鼠标光标移动到改对象上会发现它居然显示的是父类而不是新生成的子类。按理来说我们把isa指针指向新的子类后该对象应该就属于子类的实例才对。
再之后我来验证了一下,打印一下isa指针指向的类
NSLog(@"p1:%@",object_getClass(p1)); [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; NSLog(@"p1:%@",object_getClass(p1));复制代码
打印结果:
2019-01-29 14:28:40.983065+0800 KVOCustom[20946:172543] p1:Person2019-01-29 14:28:40.983401+0800 KVOCustom[20946:172543] p1:NSKVONotifying_Person复制代码
发现确实在addOberver方法调用前是指向了Person类,在调用后指向了新类NSKVONotifying_Person,从而证明了addOberver方法内部确实是创建了新类。
之后当然是想了解下这个系统创建的新的类内部实现了哪些方法啦。自己写了一个打印类内部方法如下:- (void)printMethods:(Class)cls{ unsigned int count; Method *methods = class_copyMethodList(cls, &count); NSMutableString *strM = [NSMutableString string]; [strM appendString:[NSString stringWithFormat:@"%@: ",cls]]; for (int i = 0; i < count; i++) { Method method = methods[i]; NSString *strMethodName = NSStringFromSelector(method_getName(method)); [strM appendString:strMethodName]; [strM appendString:@", "]; } NSLog(@"%@",strM);}复制代码
之后我们调用方法打印
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; [p1 printMethods:object_getClass(p1)];复制代码
打印结果:
2019-01-29 14:35:32.435852+0800 KVOCustom[21016:175547] NSKVONotifying_Person: setName:, class, dealloc, _isKVOA,复制代码
说明系统新创建的类内部实现了setName方法(用来重写父类的setter方法),class方法(为了隐藏子类),dealloc方法(释放内存),_isKVOA方法(是系统的kvo方法)
当我们探索到这里基本上也就请楚了系统的kvo的实现。
结论
- 系统调用addObserver方法时候,内部新创建了一个类NSKVONotifying_Person继承于Person类。并且改变了isa的指向,指向了这个新类。
- 新类里边重写了父类的setter方法,这样当person对象的name属性赋值时候,也就是调用了setter方法,这时候会调用子类的setter方法,该方法猜测内部调用了willChangeValueForKey,didChangeValueForKey方法,这样会调用oberver对应类的observeValueForKeyPath方法。将新旧的值都传递过来。
- 新类重写了class方法,猜测完全是为了隐藏子类的实现。当person对象调用class方法时候将返回Person类而不是新的类猜测内部实现应该是这样的:
Class classUse(id self,SEL _cmd){ return class_getSuperclass(object_getClass(self));}复制代码
- 新类调用了内部方法_isKVOA,猜测是为了告知系统使用了kvo,内部方法实现猜测是这样的:
int _isKVOAUse(id self,SEL _cmd){ return YES;}复制代码
自己手写一个KVO
现在是不是手痒痒想自己手写个KVO啦,现在我们清楚了系统KVO的实现,模仿它我们自己实现一个KVO吧。
- 首先确定这个KVO类肯定是NSObject类的分类(因为所有对象都可以调用addObserver方法)在其内部模仿系统的addObserver方法自己写一个类似的
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath valueChangeBlk:(void(^)(id old, id new))valueChangeBlk{ //创建子类 NSString *oldClass = NSStringFromClass(self.class); NSString *newClass = [NSString stringWithFormat:@"BSKVONotify_%@",oldClass]; Class classNew = objc_allocateClassPair(self.class, newClass.UTF8String, 16); objc_registerClassPair(classNew); object_setClass(self, NSClassFromString(newClass)); //新增set方法 NSMutableString *strM = [NSMutableString string]; [strM appendString:[[keyPath substringToIndex:1] uppercaseString]]; [strM appendString:[keyPath substringFromIndex:1]]; NSString *setMethod = [NSString stringWithFormat:@"set%@:",strM]; class_addMethod(NSClassFromString(newClass), NSSelectorFromString(setMethod), (IMP)keyPathMethod,"v@:@"); //新增class方法 class_addMethod(classNew, NSSelectorFromString(@"class"), (IMP)classUse,"#@:"); //新增_isKVOA方法 class_addMethod(classNew, NSSelectorFromString(@"_isKVOA"),(IMP)_isKVOAUse, "i@:"); //设置关联对象 objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_COPY); objc_setAssociatedObject(self, "blk", valueChangeBlk, OBJC_ASSOCIATION_COPY); objc_setAssociatedObject(self, "classNew", classNew, OBJC_ASSOCIATION_RETAIN); objc_setAssociatedObject(self, "classOld", self.class, OBJC_ASSOCIATION_RETAIN);}复制代码
- 在写一个对应的setter方法的函数作为属性值变化的回调
void keyPathMethod(id self,IMP _cmd, id arg){ //set方法名,原始类和子类 NSString *keyPath = objc_getAssociatedObject(self, "keyPath"); NSMutableString *strM = [NSMutableString string]; [strM appendString:[[keyPath substringToIndex:1] uppercaseString]]; [strM appendString:[keyPath substringFromIndex:1]]; NSString *setMethod = [NSString stringWithFormat:@"set%@:",strM]; Class subClass = objc_getAssociatedObject(self, "classNew"); Class oldClass = objc_getAssociatedObject(self,"classOld"); //isa指针指向父类,执行set方法 object_setClass(self, oldClass); //获取成员变量的值 Ivar ivar = class_getInstanceVariable([self class], [NSString stringWithFormat:@"_%@",keyPath].UTF8String); id value = object_getIvar(self, ivar); // NSLog(@"old:%@",value); ((id (*) (id,SEL,id))objc_msgSend)(self,NSSelectorFromString(setMethod),arg); id valueNew = arg; // NSLog(@"new:%@",valueNew); //isa指针指向子类 object_setClass(self, subClass); void(^blkUse)(id old, id new) = objc_getAssociatedObject(self, "blk"); if (blkUse) { blkUse(value,valueNew); }}复制代码
以上方法即可实现一个简单的kvo了 3. 重写class方法来隐藏内部子类
Class classUse(id self,SEL _cmd){ return class_getSuperclass(object_getClass(self));}复制代码
- 新增_isKVO方法
int _isKVOAUse(id self,SEL _cmd){ return YES;}复制代码
上述方法即可实现一个kvo了,拿去用吧!
关于系统KVO和自己实现的KVO对比的疑惑
- 最开始有提到,系统的kvo当我们打断点时候发现系统的kvo对象居然是父类就是原始类而不是新的子类。而我们的kvo对象打断点发现是新的子类而不是原始类。也就是说系统很好的隐藏了子类,而我们写的kvo却做不到,目前还不清楚这个原理。
- void keyPathMethod(id self,IMP _cmd, id arg)大家有没有注意到这里参数写的是id类型,这里的参数对应接收setter方法传递进来的参数。这个参数目前接收的是id类型,这样就有局限了,这样监听的对象的属性类型就一定是对象类型了,如果是基本类型就会崩溃。这里使用时候也要注意了,目前我不清楚该怎么改,怎么能同时接收两种类型。
- 注意循环引用的问题
p1 = [[Person alloc] init]; __weak typeof(self) weakSelf = self; [p1 addObserver:self forKeyPath:@"name" valueChangeBlk:^(id _Nonnull old, id _Nonnull new) { typeof(weakSelf) self = weakSelf; NSLog(@"self:%@,old:%@, new:%@",self,old,new); }];复制代码
这里我用__weak typeof(self) weakSelf = self;typeof(weakSelf) self = weakSelf;巧妙的解决了循环引用问题。这里我是参考MJRefresh源码,这样在block内部就可以继续使用self关键字了。 4. 关于以上的疑惑希望有人能解答,thanks!thanks!thanks!最后附上github上源代码给大家参考: