本篇来探索ReactiveCocoa的底层实现。主要从以下几个方面:
- RAC三种方式订阅是如何销毁的?
- RAC的调度者
RACScheduler
在上一个RAC基础篇
里截图简单介绍了RAC信号响应的基本流程,这幅图依然放在这里,从这个信号响应流程开始探索:
信号响应流程
大致过程如下:
在这个流程的基础上,我们继续往下探索。
一. 三种信号订阅形式,信号是如何销毁的
信号订阅的三种形式:Next
,Error
,Completed
。不同形式的订阅方式,信号的销毁有什么不同呢?
1. 三种订阅形式的使用
首先,我们创建一个信号,使用属性mySubscriber
保存订阅者,并在页面上添加三个按钮,分别发送不同形式的信号。代码如下:
//创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
//这里给了我们订阅者参数subscriber,意味着我们可以将订阅者用属性保存起来,在我们需要的地方进行信号发送。
self.mySubscriber = subscriber;
return [RACDisposable disposableWithBlock:^{
NSLog(@"销毁!!!");
}];
}];
//点击按钮发送sendNext信号
- (void)clickmyBtn {
[self.mySubscriber sendNext:@"Niki发送了一个信号"];
}
//点击按钮发送一个error信号
- (IBAction)clickErrorBtn:(id)sender {
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:404 userInfo:@{@"msg":@"网络错误"}];
[self.mySubscriber sendError:error];
}
//点击按钮发送一个完成信号
- (IBAction)clickCompleteBtn:(id)sender {
[self.mySubscriber sendCompleted];
}
下面我们先订阅Next
信号,点击发送sendNext
信号的按钮,可以看出,不论按钮点击多少次,都会收到信号的打印,并且始终不会执行NSLog(@"销毁!!!");
这句代码。也就是说,这个信号不会被销毁。
//订阅Next信号
[signal subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
}];
打印信息:
然后订阅
error
信号,点击发送error
的按钮,我们会发现,点击一次按钮,信号就销毁了。之后再点击任何的按钮都不会再做出响应。
//订阅error信号
[_signal subscribeError:^(NSError * _Nullable error) {
NSLog(@"网络错误");
}];
打印信息:
订阅
Completed
信号和error
信号是一样的。点击一次按钮,信号就销毁了。打印信息可以自己试一下,这里就不展示了。代码如下:
//订阅Completed
[_signal subscribeCompleted:^{
NSLog(@"完成");
}];
Completed
和error
分别表示完成和报错,通常我们在封装网络请求时,请求完成会使用Completed
,报错使用error
,完成和报错都意味着信号已经没有用了,考虑到性能问题,信号不用一直处在等待响应的状态,所以发生一次就销毁了。
了解了代码的呈现形式,那么我们来看一看,RAC
代码是怎么实现的。
2. 底层实现
这三个订阅方法是协议RACSubscriber
的代理方法。
在订阅信号时,订阅方法传入的
block
保存在RACSubscriber
对象中,在subscribe:
方法中,这个RACSubscriber
对象又封装成了RACPassthroughSubscriber
对象,同样遵守RACSubscriber
代理,通过调用self.didSubscribe(subscriber)
,就将参数传递给了我们自定义的属性self.mySubscriber
,这样mySubscriber
就可以调用上图的三个协议方法。代码如下:然后到
RACPassthroughSubscriber
类中看三个订阅代理方法的实现,这里三个方法是一样的,先判断信号是否已销毁,销毁了就直接返回,否则就会进入蓝色框的方法,区别在这里。innerSubscriber
是谁呢?不就是我们上面截图里的RACSubscriber *o
嘛~它也是遵循RACSubscriber
协议的呀!自己看看。进入RACSubscriber
类,看到三个代理方法的实现,我们就会发现,sendError:
和sendCompleted:
两个方法中,调用block
之前,会先调用我们的销毁者,销毁信号!于是就有了只调用一次的结果!那么还有一个问题,
sendNext:
的信号是什么时候销毁呢?我们看到
RACSubscriber
的析构函数,在这里调用了销毁者销毁,当前的mySubscriber
对象是被self强引用的,在我们当前的Controller
析构的时候,RACSubscriber
也会析构,此时,信号销毁:二. RAC的调度者RACScheduler
RAC的RACScheduler
通过自主控制线程,来实现多线程的调度。下面我们来看它是如何实现多线程控制的。
继续来到刚才看过的RACDynamicSignal
的- (RACDisposable *)subscribe:
方法中,看schedule:
方法。
这里分情况来说
- 首先不对线程进行操作,在主线程中创建以下信号:
- (void)viewDidLoad {
[super viewDidLoad];
//创建信号
_signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"hi Niki"];
return [RACDisposable disposableWithBlock:^{
NSLog(@"销毁!!!");
}];
}];
//订阅Next信号
[_signal subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
}];
}
currentScheduler:
先判断当前线程字典中有没有存储过RACScheduler
对象,我们并没有对线程进行这样的操作,所以一定是nil
,然后就会判断当前线程是否主线程,显然这里就会返回主线程的调度者。
返回之后,往回看- (RACDisposable *)schedule:
中if
判断不成立,直接执行block()
。
- 第二种情况,在子线程中创建信号:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"hi Niki"];
return [RACDisposable disposableWithBlock:^{
NSLog(@"销毁!!!");
}];
}];
//订阅Next信号
[signal subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
}];
});
}
currentScheduler:
先判断当前线程字典中有没有存储过RACScheduler
对象,我们并没有对线程进行这样的操作,所以一定是nil
,然后就会判断当前线程是否主线程,当前也非主线程,于是继续往下执行return nil;
。
返回之后,往回看- (RACDisposable *)schedule:
中if
判断成立,执行[self.backgroundScheduler schedule:block]
。
先看backgroundScheduler
是什么?在这个类的初始化方法中已经初始化了backgroundScheduler
属性。
从上图中,我们看到,初始化了
backgroundScheduler
属性的过程其实就是对GCD
的封装,就是为任务提供一个子线程,为了保证信号一个一个的执行,所以创建了一个异步串行队列。最终返回一个RACQueueScheduler
类型的对象给到backgroundScheduler
。然后回到
[self.backgroundScheduler schedule:block]
执行RACQueueScheduler
中的schedule:
方法。这样不就实现了在子线程调度了吗!看到这里是不是明白了我们的调度者
RACScheduler
做了什么。最后还有一点,刚刚判断
currentScheduler:
里面,在子线程中,没有对线程进行任何操作的时候返回nil
,才有了创建子线程的操作,那么下一次,为了让它直接得到这个调度者,于是就有了上图中的[self performAsCurrentScheduler:block];
,将当前的调度者保存到线程字典中,下一次直接返回!同时不要忘记我们的任务,在这个子线程中执行该执行的任务block()
。代码如下:后续更新销毁者、观察者等底层分析......