前面我们在使用Block的时候提到了函数式编程和链式调用的用法,但是实际上Block还有一种编程思想,就是响应式编程。
函数式编程是把相关逻辑代码写到一起,链式调用是可以使用点语法不停的调用方法,而响应式编程则是把事件回调逻辑和使用写到一起,著名的RAC框架就是响应式编程的代表。
不过不难看出,Block的灵活使用,简化了我们代码的复杂度,提升了我们编写程序的效率。
今天,我们就利用之前所学过的Block和Runtime做一个有趣的事情,就是自己写一个KVO,并且用函数式响应式编程思想进行一个改造。
-
什么是KVO
KVO即(Key - Value - Observer),是观察者模式的一种体现,它可以观察对象的一个属性,当它发生改变的时候,触发回调事件,方便我们进行逻辑操作。
我们先使用一下KVO,然后再对其进行分析。这里,我们自己创建一个类Person
,给它一个属性name
,并且对它的name
属性进行观察。为了方便测试,我们添加了一个按钮用来修改它的名字。
KVO的使用步骤:
- 给对象添加观察者
//初始化对象
self.person = [Person new];
//添加观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
- 实现回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",[change objectForKey:NSKeyValueChangeNewKey]);
}
}
- 触发回调事件
- (IBAction)changeName:(id)sender {
NSString *defaultName = @"张三";
self.person.name = [defaultName stringByAppendingFormat:@"%d",i++];
}
- 移除观察者
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}
然后,我们点击按钮就可以看到不断打印出的新name
值了:
特别需要注意的一点是,KVO只有在调用Setter方法的时候,才会进行回调,直接使用下划线修改是不会触发KVO回调的。
接着,我们再来分析一下KVO的实现原理。KVO其实是用Runtime实现的,怎么证明呢?我们在添加观察者那一行打一个断点,单步执行后,再看我们的self.person
的类型:
可以看到,
person
变量的isa
指针,指向的类型变成了NSKVONotifying_Person
,这说明KVO在运行时改变了所观察对象的类型,这个类是Person
类的子类。我们可以通过这句代码打印出它的父类来证明:
NSLog(@"%@",[NSString stringWithUTF8String:class_getName([objc_getClass("NSKVONotifying_Person") superclass])]);
前面我们知道只有Setter方法被调用才可以进入回调,那说明,这个子类一定重写了父类属性的Setter方法,并且在其中做了监听以及事件的处理。
明白了这些,我们就可以自己利用Runtime去编写一个KVO了。
首先,KVO好像谁都可以直接调用,不需要导入头文件,我们因此可以推测,它应该是NSObject
的一个Category
,我们也顺着这个思路,创建一个NSObject
的分类,我们这里命名为CBX_KVO
,并且用一个相同的方法(名字我们加一个前缀)来添加观察者:
@interface NSObject (CBX_KVO)
- (void)CBX_addObserver:(nonnull NSObject *)observer forKeyPath:(nonnull NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
接下来,我们需要在方法的实现里面做一些事情了,根据我们前面的分析,我们要动态创建一个新类,这个类是观察对象的子类,并且重写它对应属性的Setter
方法:
- (void)CBX_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
//创建一个观察对象的子类
NSString *oldName = NSStringFromClass(self.class);
NSString *newName = [@"CBX_KVO_" stringByAppendingString:oldName];
Class NewClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
//注册新类
objc_registerClassPair(NewClass);
//改变self的类型
object_setClass(self, NewClass);
//为其添加一个Setter方法
class_addMethod(NewClass, @selector(setName:), (IMP)newSetter, "v@:@");
}
void newSetter(NSString * newName){
NSLog(@"%@",newName);
}
写到这里,我们可以先测试一下,添加我们自己的观察者,每次点击按钮改变name
属性的时候能不能打印出新的值:
[self.person CBX_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
但是实际上,打印出来的并不是我们想要的改变以后的
name
值,而是这个对象自己,这是因为OC方法都有两个隐藏的参数:self
和_cmd
,我们自己写的函数并没有添加这两个参数,所以读出来的默认是第一个参数。接下来,我们改变一下函数:
void newSetter(id self,SEL _cmd,NSString * newName){
NSLog(@"%@",newName);
}
再测试一下:
现在可以顺利打印出值了。然后我们就可以在自己写的函数里利用Block回调来简化我们的KVO。
首先,我们需要在前面的
CBX_addObserver
方法添加一个Block参数来回调处理后续逻辑。然后在修改了属性以后,在函数内部调用Block,就可以实现回调的效果了。
但是怎么在我们写的函数里面获取Block呢?可以有两种方法,第一种是把block当做参数传下去,另外一种就是把block作为属性添加给新类,然后函数内部可以通过
self
获取并调用。我们这里使用第二种:
- (void)CBX_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block:(void (^)(NSString *newName))block{
//创建一个观察对象的子类
NSString *oldName = NSStringFromClass(self.class);
NSString *newName = [@"CBX_KVO_" stringByAppendingString:oldName];
Class NewClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
//注册新类
objc_registerClassPair(NewClass);
//改变self的类型
object_setClass(self, NewClass);
//为其添加一个Setter方法
class_addMethod(NewClass, @selector(setName:), (IMP)newSetter, "v@:@");
//为self增加block属性
objc_setAssociatedObject(self, @"1", block, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
void newSetter(id self,SEL _cmd,NSString * newName){
//处理回调
void(^block)(NSString *newname) = objc_getAssociatedObject(self, @"1");
block(newName);
}
现在我们使用一下,就可以在block回调中得到新的属性值了:
[self.person CBX_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil block:^(NSString *newName) {
NSLog(@"%@",newName);
}];
但是,别以为现在就完了,我们只是把新的值从回调返回,但是并没有为其赋值,所以此时对象的name
属性的值还是nil
。
我们需要在自己写的Setter函数内为其赋值,但是直接使用Setter方法是不行的(会循环调用),这地方要调用父类的方法才行,而这个地方有没办法直接调用父类的方法,所以我们用消息发送来调用:
void newSetter(id self,SEL _cmd,NSString * newName){
//调用父类方法
struct objc_super mySuper;
mySuper.receiver = self;
mySuper.super_class = [self superclass];
objc_msgSendSuper(&mySuper, @selector(setName:), newName);
//处理回调
void(^block)(NSString *newname) = objc_getAssociatedObject(self, @"1");
block(newName);
}
大功告成!现在我们就可以在block中处理回调而不用再实现回调方法了!但是实际上,我们只是简单的实现了KVO,我们并没有对类型,方法名等做适配,这些太复杂了,这里就不做具体展示了(其实我也不会),Demo我放在了github上,点击前往。
网友评论