在阅读本文之前,强烈安利以下三篇文章:
细说ReactiveCocoa的冷信号与热信号(一)
细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号
细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号
在RAC4中,Singal
对应RAC2中的RACSubject
即为热信号,而SignalProducer
对应RAC2中的RACSignal
即为了冷信号。冷信号和热信号的区别,可参考以上三篇文章。
1. 热信号Signal
为了验证Signal
具有热信号的特点,进行如下实验
let (signal, observer) = Signal<String, NSError>.pipe()
//订阅者1
QueueScheduler.mainQueueScheduler.scheduleAfter(0.1) {
signal.observeNext {
NSLog("Subscriber 1 get a next value: \($0) from signal")
}
}
//订阅者2
signal.observeNext {
NSLog("Subscriber 2 get a next value: \($0) from signal")
}
//开始发送第一个包
QueueScheduler.mainQueueScheduler.scheduleAfter(1) {
NSLog("signal send package 1"); observer.sendNext("send package 1")
}
//订阅者3
QueueScheduler.mainQueueScheduler.scheduleAfter(1.1) {
signal.observeNext {
NSLog("Subscriber 3 get a next value: \($0) from signal")
}
}
//发送第二个包
QueueScheduler.mainQueueScheduler.scheduleAfter(2) {
NSLog("signal send package 2"); observer.sendNext("send package 2")
}
按照时间顺序来解释上述代码:
- 0s 创建
signal
这个热信号 - 立即订阅该信号,该订阅者命名为订阅者2
- 0.1s订阅刚刚创建的热信号
Signal
,该订阅者命名为订阅者1 - 1s
signal
发送第一个包 - 1.1s后订阅该信号,该订阅者命名为订阅者3
- 2s后
signal
发送第二个包
看一下程序的运行情况
2016-04-19 17:37:30.242 ColdSignalAndHotSignal[29298:3700008] Start
2016-04-19 17:37:30.245 ColdSignalAndHotSignal[29298:3700008] Subscriber 2 subscribe
2016-04-19 17:37:30.366 ColdSignalAndHotSignal[29298:3700008] Subscriber 1 subscribe
2016-04-19 17:37:31.295 ColdSignalAndHotSignal[29298:3700008] signal send package 1
2016-04-19 17:37:31.298 ColdSignalAndHotSignal[29298:3700008] Subscriber 2 get a next value: send package 1 from signal
2016-04-19 17:37:31.299 ColdSignalAndHotSignal[29298:3700008] Subscriber 1 get a next value: send package 1 from signal
2016-04-19 17:37:31.434 ColdSignalAndHotSignal[29298:3700008] Subscriber 3 subscribe
2016-04-19 17:37:32.246 ColdSignalAndHotSignal[29298:3700008] signal send package 2
2016-04-19 17:37:32.246 ColdSignalAndHotSignal[29298:3700008] Subscriber 2 get a next value: send package 2 from signal
2016-04-19 17:37:32.246 ColdSignalAndHotSignal[29298:3700008] Subscriber 1 get a next value: send package 2 from signal
2016-04-19 17:37:32.246 ColdSignalAndHotSignal[29298:3700008] Subscriber 3 get a next value: send package 2 from signal
分析输出结果:
- 30.242s,
signal
创建,开始计时 - 30.245s,即0s后,订阅者2订阅
signal
- 30.366s,即0.1s后,订阅者1订阅
signal
,可以看到订阅者1、2的订阅时间相差约0.1s - 31.295s,即1s后,
signal
发送第一个包 - 31.298s,即1s后,订阅者2接收到
signal
发送的包,并打印出来。可以看到signal
一发送,订阅者立刻就接受到 - 31.299s,即1s后,订阅者1接收到
signal
发送的包,并打印出来。几乎和订阅者2同时接收到包 - 31.434s,即1.1s后,订阅者3订阅
signal
,注意订阅者3并没有收到第一个包 - 32.246s,即2s后,
signal
发送第二个包 - 32.246s,即2s后,订阅者2立即接收到
signal
发送的包 - 32.246s,即2s后,订阅者1立即接收到
signal
发送的包 - 32.246s,即2s后,订阅者3立即接收到
signal
发送的包
根据上面的分析结果可知:
- 订阅者对热信号没有任何影响,无论是否有订阅者订阅热信号,热信号都会发送事件
- 订阅者几乎是同时接收到信号发出的事件(忽略微小的延迟)
- 如果订阅者在热信号发送事件之后在订阅,订阅者无法接收到订阅之前的事件
因此,根据热信号的特点,可以得到下图:
2. 冷信号SignalProducer
同样为了验证SignalProducer
具有冷信号的特点,进行如下实验
NSLog("producer start")
let producer = SignalProducer<String, NSError> {
sink, _ in
//开始发送第一个包
QueueScheduler.mainQueueScheduler.scheduleAfter(1) {
NSLog("producer send package 1"); sink.sendNext("send package 1")
}
//发送第二个包
QueueScheduler.mainQueueScheduler.scheduleAfter(2) {
NSLog("producer send package 2"); sink.sendNext("send package 2")
}
}
//订阅者1
QueueScheduler.mainQueueScheduler.scheduleAfter(2) {
NSLog("Subscriber 1")
producer.startWithNext {
NSLog("Subscriber 2 get a next value: \($0) from producer")
}
}
//订阅者2
QueueScheduler.mainQueueScheduler.scheduleAfter(3) {
NSLog("Subscriber 2")
producer.startWithNext {
NSLog("Subscriber 2 get a next value: \($0) from producer")
}
}
按照时间顺序来解释上述代码:
- 0s创建冷信号
producer
- 1s
producer
发送第一个包 - 2s
producer
发送第二个包 - 2s,订阅冷信号
producer
该订阅者命名为订阅者1 - 3s,订阅冷信号
producer
该订阅者命名为订阅者2
2016-04-20 10:43:45.683 ColdSignalAndHotSignal[32370:4197730] producer start
2016-04-20 10:43:47.886 ColdSignalAndHotSignal[32370:4197730] Subscriber 1
2016-04-20 10:43:48.685 ColdSignalAndHotSignal[32370:4197730] Subscriber 2
2016-04-20 10:43:48.889 ColdSignalAndHotSignal[32370:4197730] producer send package 1
2016-04-20 10:43:48.892 ColdSignalAndHotSignal[32370:4197730] Subscriber 1 get a next value: send package 1 from producer
2016-04-20 10:43:49.685 ColdSignalAndHotSignal[32370:4197730] producer send package 1
2016-04-20 10:43:49.686 ColdSignalAndHotSignal[32370:4197730] Subscriber 2 get a next value: send package 1 from producer
2016-04-20 10:43:49.890 ColdSignalAndHotSignal[32370:4197730] producer send package 2
2016-04-20 10:43:49.890 ColdSignalAndHotSignal[32370:4197730] Subscriber 1 get a next value: send package 2 from producer
2016-04-20 10:43:50.686 ColdSignalAndHotSignal[32370:4197730] producer send package 2
2016-04-20 10:43:50.686 ColdSignalAndHotSignal[32370:4197730] Subscriber 2 get a next value: send package 2 from producer
分析输出结果
- 45.683s,创建冷信号
- 47.886s,即2s后,订阅者1订阅冷信号
producer
- 48.685s,即3s后,订阅者2订阅冷信号
producer
- 48.889s,即3s后,
producer
发送第一个包,(为什么是在3s后发送?) - 48.892s,即3s后,与此同时,订阅者1接收到
producer
发出的第一个包 - 49.685s,即4s后,
producer
再次发送第一个包(为什么又发送一次?) - 49.686s,即4s后,与此同时,订阅者2接收到
producer
发送的第一个包 - 49.890s,即4s后,
producer
发送第二个包 - 49.890s,即4s后,与此同时,订阅者1接收到
producer
发出的第二个包 - 50.686s,即5s后,
producer
再次发送第二个包 - 50.686s,即5s后,与此同时,订阅者2接收到
producer
发出的第二个包
虽然输出结果混合在一起,但通过分析可以得知4、6提出的问题
为什么producer是在
3s后发送第一个包?
因为,订阅者1是在2s后才订阅冷信号producer
,然后producer
在1s后发给订阅者1第一个包(注意:是发给订阅者1),这也解释了为什么producer
每个包会发两遍
为什么又发送一次?
producer
再次发送第一个包是发送给订阅者2的,而订阅者2是在3s后才订阅冷信号producer
,然后producer
在1s后发给订阅者2第一个包
上面分析也证明了SignalProducer
具有冷信号的特点
- SignalProducer`是一对一发送,这句话可能不好理解。这里可以理解成,有几个订阅者,冷信号就发送几次同样的信号
- 每个订阅者都能接收到同样的事件。例如上面订阅者2在3s后订阅,那它就在4s后和5s后接收到事件
因此,根据冷信号的特点,可以得到下图:
细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号有一句话很形象地说明了冷热信号的特点:
热信号类似“直播”,错过了就不再处理。而冷信号类似“点播”,每次订阅都会从头开始。
由上述分析,可以得知RAC2和RAC4中的冷热信号有如下关系:
RAC2 | RAC4 | |
---|---|---|
冷信号 | RACSignal | SignalProducer |
热信号 | RACSubject | Singal |
3. 冷信号的副作用(Side Effect)
在细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号提出了,如果冷信号中包含网络请求,那么每次订阅这个冷信号都会发送网络请求,而且任何的信号转换即是对原有的信号进行订阅从而产生新的信号
对于上述遇到的副作用,有两种解决办法:
- 确保只对冷信号进行一次订阅
- 将冷信号转换成特殊的热信号
对于解决方法1,对于一些简单的业务逻辑适用,获得冷信号,订阅冷信号,处理发出的事件。然而对于一些逻辑复杂的场景,需要对返回的信号进行多次处理,就需要对冷信号进行转换,以避免副作用
4. 特殊的热信号
为什么是特殊的热信号?特殊在哪?和普通的热信号又要什么区别?在解释这些问题之前,先看一个简单的实验
let signal = signalFromNetwork()
signal.observeNext {NSLog("subscriber get a next value: \($0)")}
这里假设signalFromNetwork()
是发送网络请求后获得的一个热信号signal
(注意是热信号),然后订阅该信号,这里简单地打印事件。
但是,如果运行这段代码,并没有输出任何结果。是因为返回的信号没有发送任何next
事件吗?
func signalFromNetwork() -> Signal<String, NSError> {
return Signal<String, NSError> {
observer in
NSLog("signal send hello")
observer.sendNext("Hello")
return nil
}
}
实际上,返回的热信号发送了一个next
事件,但是订阅者没有收到。
我们把上面代码稍微改一下
func signalFromNetwork() -> Signal<String, NSError> {
return Signal<String, NSError> {
observer in
QueueScheduler.mainQueueScheduler.scheduleAfter(1) {
NSLog("signal send hello")
observer.sendNext("Hello")
}
return nil
}
}
即,信号1s后再发送事件,在看代码的运行结果
2016-04-20 14:59:45.150 ColdSignalAndHotSignal[35259:4813102] signal send hello
2016-04-20 14:59:45.153 ColdSignalAndHotSignal[35259:4813102] subscriber get a next value: Hello
这是我们可以看到有了输出结果。这就说明了之前订阅者为什么没有接收到事件,因为在订阅者订阅热信号之前,热信号就已经发送了事件。而这次是因为热信号延迟了1s才发送事件,所以订阅者才能接收到数据
虽然,我们可以让生成信号的时候,延迟一段时间在发送事件,但RAC提供一种更好的热信号来处理这种情况。
这就是RAC2中的RACReplaySubject
,这种信号特点在于:
- 无论是否有订阅者订阅该信号,该信号都会发送事件,这点和热信号一致
- 无论订阅者何时订阅信号,订阅者都能立即接收到该信号所发送的事件,这点和冷信号相似,但有很大的不同
而在RAC4中,我们使用SignalProducer.buffer(Int)
这个方法来代替RACReplaySubject
Using SignalProducer.buffer instead of replaying
同样通过一个实验来证明SignalProducer.buffer
和RACReplaySubject
具有同样的特点
let (producer, sink) = SignalProducer<String, NSError>.buffer(Int.max)
NSLog("producer start!")
//订阅者1
producer.startWithNext {
NSLog("Subscriber 1 get a next value: \($0) from producer")
}
//开始发送第一个包
QueueScheduler.mainQueueScheduler.scheduleAfter(1) {
NSLog("producer send package 1"); sink.sendNext("send package 1")
}
//发送第二个包
QueueScheduler.mainQueueScheduler.scheduleAfter(2) {
NSLog("producer send package 2"); sink.sendNext("send package 2")
}
//订阅者2
QueueScheduler.mainQueueScheduler.scheduleAfter(4) {
producer.startWithNext {
NSLog("Subscriber 2 get a next value: \($0) from producer")
}
}
因为代码和之前实验代码相似,这里只简单解释下:1
- 创建特殊的热信号,并在1s,4s后发送两个包
- 在0s和1.5s时,订阅者1、2订阅了该信号
以下是输出结果:
2016-04-20 15:31:23.821 ColdSignalAndHotSignal[35872:4955068] producer start!
2016-04-20 15:31:24.861 ColdSignalAndHotSignal[35872:4955068] producer send package 1
2016-04-20 15:31:24.863 ColdSignalAndHotSignal[35872:4955068] Subscriber 1 get a next value: send package 1 from producer
2016-04-20 15:31:26.037 ColdSignalAndHotSignal[35872:4955068] producer send package 2
2016-04-20 15:31:26.037 ColdSignalAndHotSignal[35872:4955068] Subscriber 1 get a next value: send package 2 from producer
2016-04-20 15:31:28.237 ColdSignalAndHotSignal[35872:4955068] Subscriber 2 get a next value: send package 1 from producer
2016-04-20 15:31:28.237 ColdSignalAndHotSignal[35872:4955068] Subscriber 2 get a next value: send package 2 from producer
来分析输出结果的一些特点:
- 所有事件,信号只发送了一次
- 订阅者1是0s订阅的,毫无疑问,订阅者1可以接收到所有事件
- 订阅者2是4s才订阅的,而此时信号已经发出了所有的事件,如果是普通的热信号,订阅者2是接受不到任何事件的,但这里订阅者2却同时接收到了信号发送的所有事件,就像所有的事件缓存起来一样
根据特点,我们可以得到ReplaySubject
和SingalProducer.buffer
产生的信号的图
而且这种信号的命名也很有意思,在RAC2中是Replay,代表事件可以重放。而在RAC4中是buffer,代表事件被缓存起来
现在回到冷信号副作用的问题上,因为buffer
返回的信号,具有热信号的特点,不会产生副作用。同时又能像冷信号一样,确保所有的订阅者都能接收到事件。
现在将本节signalFromNetwork()
作出一些更改
func signalFromNetwork() -> SignalProducer<String, NSError> {
let (producer, sink) = SignalProducer<String, NSError>.buffer(Int.max)
NSLog("signal send hello")
sink.sendNext("Hello")
return producer
}
然后来订阅返回的信号
let signal = signalFromNetwork()
QueueScheduler.mainQueueScheduler.scheduleAfter(2) {
NSLog("Subscriber 1")
signal.startWithNext{NSLog("Subscriber get a next value: \($0)")}
}
QueueScheduler.mainQueueScheduler.scheduleAfter(4) {
NSLog("Subscriber 2")
signal.startWithNext{NSLog("subscriber 2 get a next value: \($0)")}
}
为了突出效果,我们故意使用两个订阅者订阅了该信号,并且一个延时到2s才订阅,一个延时到4s才订阅,来看下输出结果:
2016-04-20 15:48:50.192 ColdSignalAndHotSignal[36260:5027885] signal send hello
2016-04-20 15:48:52.394 ColdSignalAndHotSignal[36260:5027885] Subscriber 1
2016-04-20 15:48:52.397 ColdSignalAndHotSignal[36260:5027885] Subscriber get a next value: Hello
2016-04-20 15:48:54.594 ColdSignalAndHotSignal[36260:5027885] Subscriber 2
2016-04-20 15:48:54.595 ColdSignalAndHotSignal[36260:5027885] subscriber 2 get a next value: Hello
和预期的一样,无论有多少个订阅者,信号都只会发送一次事件,而且无论订阅者多迟才订阅该信号都能收到信号发送的事件。
然而,有些情况下,类似signalFromNetwork()
这种方法是别人提供的,而且返回的就是一个冷信号SignalProducer
这种情况,你可能无法修改signalFromNetwork()
内部代码。那要如何处理这个冷信号,避免副作用呢?
在RAC4.0,SignalProducer添加了replayLazily
这个方法,避免了冷信号的副作用 Added SignalProducer.replayLazily for multicasting
我们将signalFromNetwork()
改成返回冷信号
func signalFromNetwork() -> SignalProducer<String, NSError> {
let producer = SignalProducer<String, NSError> {
observer, _ in
NSLog("signal send hello")
observer.sendNext("Hello")
}
return producer
}
而订阅该信号的代码如下:
let signal = signalFromNetwork().replayLazily(1)
QueueScheduler.mainQueueScheduler.scheduleAfter(2) {
NSLog("Subscriber 1")
signal.startWithNext{NSLog("Subscriber get a next value: \($0)")}
}
QueueScheduler.mainQueueScheduler.scheduleAfter(4) {
NSLog("Subscriber 2")
signal.startWithNext{NSLog("subscriber 2 get a next value: \($0)")}
}
只是在返回信号调用replayLazily
方法,其余都不变
2016-04-20 16:11:54.223 ColdSignalAndHotSignal[36783:5133284] start
2016-04-20 16:11:56.423 ColdSignalAndHotSignal[36783:5133284] Subscriber 1
2016-04-20 16:11:56.429 ColdSignalAndHotSignal[36783:5133284] signal send hello
2016-04-20 16:11:56.429 ColdSignalAndHotSignal[36783:5133284] Subscriber get a next value: Hello
2016-04-20 16:11:58.623 ColdSignalAndHotSignal[36783:5133284] Subscriber 2
2016-04-20 16:11:58.623 ColdSignalAndHotSignal[36783:5133284] subscriber 2 get a next value: Hello
貌似和之前和输出结果有点不一样,忽略那个start
吧!只有Subscriber1
和signal send hello
顺序颠倒了,从时间上来看,信号发送事件的时间延迟了。
5. buffer
和replayLazily
中的参数capacity
在使用这两个方法时,需要传一个名为capacity
参数,那这个参数是什么意思呢?感兴趣的同学可以先去看看官方文档是怎么解释的。
这里通过小实验来研究这个参数的意义
let (producer, sink) = SignalProducer<String, NSError>.buffer(1)
sink.sendNext("A")
sink.sendNext("B")
sink.sendNext("C")
QueueScheduler.mainQueueScheduler.scheduleAfter(2) {
producer.startWithNext{print($0)}
}
很简单的一段代码,信号发送三个next
事件,2s后,订阅者订阅该信号。
如果你以为输出的是A B C,那就请看实际运行结果
C
貌似只输出一个C,那A和B呢
接下来,我们把buffer(1)
改成buffer(3)
在看输出结果
A
B
C
到这就应该明白capacity
的含义,就是指SignalProducer
为订阅者缓存多少个事件,如果发送事件数超过缓存容量,则先发送的事件会被后发送的事件覆盖,这也解释了为什么当capacity=1
时,只输出C
同样replayLazily
中的capacity
参数也是同样的含义
但是,如果将上面的例子改成replayLazily
,输出结果会有略微的不同,自己分析原因吧
`