前言
我们在使用ReactiveCocoa
的时候,对RACSignal
以及RACSubject
的订阅非常频繁,虽然订阅的代码写起来并不繁琐,但里面也会存在一些比较细的问题,这篇文章主要针对信号在订阅中出现的某些问题进行一系列的分析。
RACSignal的重复订阅
在这里我首先创建一个普通的信号:
RACSignal *signalOne = [RACSignal createSignal:^RACDisposable *(id subscriber) {
static NSInteger flag = 0;
[subscriber sendNext:@(flag++)];
return nil;
}];
从这个信号的构建中我们可以看出,当程序每次执行这个信号的构建block时,里面设置的静态整数标识符都会被以信号事件的形式发射出去,并且每次被发射,它都会进行自增。
下面我们来对这个信号进行重复订阅:
[signalOne subscribeNext:^(NSNumber *x) {
NSLog(@"Subscriber1--%ld",[x integerValue]);
}];
[signalOne subscribeNext:^(NSNumber *x) {
NSLog(@"Subscriber2--%ld",[x integerValue]);
}];
[signalOne subscribeNext:^(NSNumber *x) {
NSLog(@"Subscriber3--%ld",[x integerValue]);
}];
在这里我创建了三个订阅者来对这一个事件进行重复订阅,并且在订阅block中进行输入事件的打印。
跑起来,让我们看看控制台输出了什么:
Subscriber1--0
Subscriber2--1
Subscriber3--2
可见,当我们每次对这个信号进行订阅时,信号的创建block都会执行一次。
如果这时候我有一个需求:不管有多少个订阅者,信号的构建block都只执行一次,订阅的事件都是这次构建block所发送的事件。我该怎么做?
这里我可以使用两种方法:
RACMulticastConnection
使用方法:
- 在这里我创建一个信号,它里面的构建block跟上面的
signalOne
一样:
RACSignal *signalTwo = [RACSignal createSignal:^RACDisposable *(id subscriber) {
static NSInteger flag = 0;
[subscriber sendNext:@(flag++)];
return nil;
}];
- 以新创建的信号为基础,创建一个
RACMulticastConnection
:
RACMulticastConnection *connection = [signalTwo publish];
- 利用新创建的
RACMulticastConnection
取出其信号属性进行订阅:
RACSignal *connectionSignal = connection.signal;
[connectionSignal subscribeNext:^(NSNumber *x) {
NSLog(@"Subscriber1--%ld",[x integerValue]);
}];
[connectionSignal subscribeNext:^(NSNumber *x) {
NSLog(@"Subscriber2--%ld",[x integerValue]);
}];
[connectionSignal subscribeNext:^(NSNumber *x) {
NSLog(@"Subscriber3--%ld",[x integerValue]);
}];
- 开始连接:
[connection connect];
注意最后一步不能够漏掉,仅仅实现了之前的三步并不会让connectionSignal
发送事件,因为当你执行connect
方法时,connection
才会去订阅源信号,所以connectionSignal
实质上其实是一个RACSubject
,当源信号发送事件时,相应的connectionSignal
会调用sendNext:
。
现在运行程序,看看控制台打印出什么结果:
Subscriber1--0
Subscriber2--0
Subscriber3--0
可见,运用此方法,源信号signalTwo
的创建block只执行了一次。
Replay、ReplayLast、ReplayLazily
这三个是RACSignal
都可以使用的方法,它们的返回值也是一个信号,并且对于RACSignal
,replay
与replayLazily
的效果一样,所以接下来我主要讲解replay
和replayLast
相关使用以及区别:
- 创建两个信号,它们的构建block与前面不同的是里面会再发送一次带有
flag
的事件:
RACSignal *signalThree = [RACSignal createSignal:^RACDisposable *(id subscriber) {
static NSInteger flag = 0;
[subscriber sendNext:@(flag++)];
[subscriber sendNext:@(flag++)];
return nil;
}];
RACSignal *signalFour =[RACSignal createSignal:^RACDisposable *(id subscriber) {
static NSInteger flag = 0;
[subscriber sendNext:@(flag++)];
[subscriber sendNext:@(flag++)];
return nil;
}];
- 基于前面创建的两个信号,利用
replay
以及replayLast
分别再引出两个信号signalFive
、signalSix
:
RACSignal *signalFive = [signalThree replay];
RACSignal *signalSix = [signalFour replayLast];
- 分别对
signalFive
、signalSix
进行两次订阅:
[signalFive subscribeNext:^(NSNumber *x) {
NSLog(@"Subscriber1-1---%ld",[x integerValue]);
}];
[signalFive subscribeNext:^(NSNumber *x) {
NSLog(@"Subscriber1-2---%ld",[x integerValue]);
}];
// --------------------------------------
[signalSix subscribeNext:^(NSNumber *x) {
NSLog(@"Subscriber2-1---%ld",[x integerValue]);
}];
[signalSix subscribeNext:^(id x) {
NSLog(@"Subscriber2-2---%ld",[x integerValue]);
}];
```
4. 运行程序,看看控制台的输出:
Subscriber1-1---0
Subscriber1-1---1
Subscriber1-2---0
Subscriber1-2---1
Subscriber2-1---1
Subscriber2-2---1
由此可得结论:
- 当我们使用`replay`处理信号后,不管订阅者有多少个,信号的构建block都只会执行一次,且信号的构建block里若有N次事件发送,订阅者就会接收到N次的事件。
- 当我们使用`replayLast`处理信号后,不管订阅者有多少个,信号的构建block都只会执行一次,其不管信号的构建block里有多少次事件发送,订阅者都只会接收到信号最后一次发送的事件。
## RACSubject的信号重接收订阅
对于`RACSubject`的订阅来说,其有两种订阅顺序:
- 对subject信号的订阅在其`sendNext:`之前
- 对subject信号的订阅在其`sendNext:`之后
对于第一种情况,若订阅者先订阅了subject,在之后subject才去发送事件,这样子的话所有订阅者都会接收到此subject发送的全部事件,所以对于这种情况我们不进行讨论。
我们现在针对第二种情况对某个`RACSubject`进行普通的订阅:
1. 在这里我们创建一个`RACSubject`,并让他先发送三个事件,然后我们对它进行订阅:
```objc
RACSubject *subjectOne = [RACSubject subject];
[subjectOne sendNext:@(0)];
[subjectOne subscribeNext:^(NSNumber *x) {
NSLog(@"Subscribe--%ld",[x integerValue]);
}];
- 让我们让程序跑起来,我们会看到控制台并无打印。
可见,对于RACSubject
,若我们在其发送事件之后进行普通的订阅,订阅者并不会收到它发送的事件。
若我们想做到在RACSubject
发送事件后进行订阅,且又能接收到它在被订阅前发送的事件,我们应该怎么办呢?
这时候也是有两个方法供君选择:
Replay
、ReplayLast
、ReplayLazily
在文章前面我也说到过这三个方法也可用于RACSignal
的重复订阅,其实在RACSubject
中这三个方法的效果跟RACSignal
也非常相似,现在就让我们来看一下:
首先,我们先对replay
和replayLast
进行分析:
- 创建一个
RACSubject
,利用replay
和replayLast
从它里面引出两个信号,并让它发送两个带有整数的事件:
RACSubject *subjectTwo = [RACSubject subject];
RACSignal *replaySignal = subjectTwo.replay;
RACSignal *replayLastSignal = subjectTwo.replayLast;
[subjectTwo sendNext:@(0)];
[subjectTwo sendNext:@(1)];
注意,利用replay
和replayLast
引出信号的这两个语句不能够写在subject发送事件之后,不然的话订阅者就无法接收到那些事件了。
- 分别订阅两个引出信号:
[replaySignal subscribeNext:^(NSNumber *x) {
NSLog(@"One--%ld",[x integerValue]);
}];
[replayLastSignal subscribeNext:^(NSNumber *x) {
NSLog(@"Two--%ld",[x integerValue]);
}];
- 现在,我们运行程序,看看控制台输出了什么:
One--0
One--1
---------
Two--1
由此可见:
- 当我们订阅被
replay
进行处理的RACSubject
后,订阅者会收到它在订阅前RACSubject
发送的全部事件。 - 当我们订阅被
replayLast
进行处理的RACSubject
后,订阅者会收到它在订阅前RACSubject
最后一次发送的事件。
现在我们再看看replayLazily
其实replayLazily
跟replay
是非常相似的,都可以让订阅者接收到RACSubject
被订阅前发送的所有事件,但是,replayLazily
比起后者多出了"Lazily",我们可从它的命名知道它是具有"懒惰性"的,现在我就通过一个例子来对比一下replayLazily
和replay
:
- 我们先创建一个
RACSubject
,利用replay
和replayLazily
从它里面引出两个信号,并让它发送一个带有整数"0"的事件:
RACSubject *subjectThree = [RACSubject subject];
RACSignal *replaySignal = subjectThree.replay;
RACSignal *replayLazilySignal = subjectThree.replayLazily;
[subjectThree sendNext:@(0)];
- 然后我们分别对两个引出信号进行订阅:
[replaySignal subscribeNext:^(NSNumber *x) {
NSLog(@"One--%ld",[x integerValue]);
}];
[replayLazilySignal subscribeNext:^(NSNumber *x) {
NSLog(@"Two--%ld",[x integerValue]);
}];
- 现在我们运行程序,查看控制台的打印:
One--0
可见,与replay
相比,使用replayLazily
引出的信号在订阅后没有接收到在其订阅前RACSubject
发送的事件。
现在我们将代码改一改,在subject发送事件的前面再插一个订阅的语句,最终就是这样子:
RACSubject *subjectThree = [RACSubject subject];
RACSignal *replaySignal = subjectThree.replay;
RACSignal *replayLazilySignal = subjectThree.replayLazily;
[replayLazilySignal subscribeNext:^(NSNumber *x) {
NSLog(@"Zero--%ld",[x integerValue]);
}];
[subjectThree sendNext:@(0)];
其他的代码我们不需要修改,然后现在我们运行一下程序,看看控制台的输出:
Zero--0
One--0
Two--0
现在我们可以看到,事件发送前后的replayLazily
信号都可以接收到事件,由此我们可以得出结论:
利用replayLazily
从RACSubject
里引出的信号,存在"懒惰性",这个信号开始可以理解为一个"冷信号",需要在RACSubject
发送事件前先被订阅一次,让其激活成为"热信号",然后才可以在RACSubject
发送事件后再进行订阅,这样子就能够接收到订阅前subject发送的信号。
RACReplaySubject
RACReplaySubject
可以充当上面的replay
以及replayLast
方法,接收订阅前发送的全部事件或者最后一个事件,不仅如此,你还能设置让它接收特定数量的事件,下面我就展示一下它的使用:
- 我们用另一种构建方法构建一个
RACReplaySubject
,通过设置capacity
来限定它接收重接收事件的数量,并让它发送三个事件:
RACReplaySubject *replaySubject = [RACReplaySubject replaySubjectWithCapacity:2];
// [RACReplaySubject subject];
[replaySubject sendNext:@(0)];
[replaySubject sendNext:@(1)];
[replaySubject sendNext:@(3)];
- 订阅
replaySubject
:
[replaySubject subscribeNext:^(NSNumber *x) {
NSLog(@"Subscriber--%ld",[x integerValue]);
}];
- 运行程序,查看控制台打印:
Subscriber--1
Subscriber--3
可见,RACReplaySubject
会接收最新的两条事件。
参考资料
Comparing replay, replayLast, and replayLazily
最快让你上手ReactiveCocoa之基础篇