ReactiveCocoa4中的冷信号和热信号

在阅读本文之前,强烈安利以下三篇文章:
细说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")
}

按照时间顺序来解释上述代码:

  1. 0s 创建signal这个热信号
  2. 立即订阅该信号,该订阅者命名为订阅者2
  3. 0.1s订阅刚刚创建的热信号Signal,该订阅者命名为订阅者1
  4. 1ssignal发送第一个包
  5. 1.1s后订阅该信号,该订阅者命名为订阅者3
  6. 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

分析输出结果:

  1. 30.242s, signal创建,开始计时
  2. 30.245s,即0s后,订阅者2订阅signal
  3. 30.366s,即0.1s后,订阅者1订阅signal,可以看到订阅者1、2的订阅时间相差约0.1s
  4. 31.295s,即1s后,signal发送第一个包
  5. 31.298s,即1s后,订阅者2接收到signal发送的包,并打印出来。可以看到signal一发送,订阅者立刻就接受到
  6. 31.299s,即1s后,订阅者1接收到signal发送的包,并打印出来。几乎和订阅者2同时接收到包
  7. 31.434s,即1.1s后,订阅者3订阅signal注意订阅者3并没有收到第一个包
  8. 32.246s,即2s后,signal发送第二个包
  9. 32.246s,即2s后,订阅者2立即接收到signal发送的包
  10. 32.246s,即2s后,订阅者1立即接收到signal发送的包
  11. 32.246s,即2s后,订阅者3立即接收到signal发送的包

根据上面的分析结果可知:

  1. 订阅者对热信号没有任何影响,无论是否有订阅者订阅热信号,热信号都会发送事件
  2. 订阅者几乎是同时接收到信号发出的事件(忽略微小的延迟)
  3. 如果订阅者在热信号发送事件之后在订阅,订阅者无法接收到订阅之前的事件

因此,根据热信号的特点,可以得到下图:


热信号.png

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")
    }
}

按照时间顺序来解释上述代码:

  1. 0s创建冷信号producer
  2. 1sproducer发送第一个包
  3. 2sproducer发送第二个包
  4. 2s,订阅冷信号producer该订阅者命名为订阅者1
  5. 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

分析输出结果

  1. 45.683s,创建冷信号
  2. 47.886s,即2s后,订阅者1订阅冷信号producer
  3. 48.685s,即3s后,订阅者2订阅冷信号producer
  4. 48.889s,即3s后,producer发送第一个包,(为什么是在3s后发送?)
  5. 48.892s,即3s后,与此同时,订阅者1接收到producer发出的第一个包
  6. 49.685s,即4s后,producer再次发送第一个包(为什么又发送一次?)
  7. 49.686s,即4s后,与此同时,订阅者2接收到producer发送的第一个包
  8. 49.890s,即4s后,producer发送第二个包
  9. 49.890s,即4s后,与此同时,订阅者1接收到producer发出的第二个包
  10. 50.686s,即5s后,producer再次发送第二个包
  11. 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具有冷信号的特点

  1. SignalProducer`是一对一发送,这句话可能不好理解。这里可以理解成,有几个订阅者,冷信号就发送几次同样的信号
  2. 每个订阅者都能接收到同样的事件。例如上面订阅者2在3s后订阅,那它就在4s后和5s后接收到事件

因此,根据冷信号的特点,可以得到下图:


冷信号.png

细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号有一句话很形象地说明了冷热信号的特点:

热信号类似“直播”,错过了就不再处理。而冷信号类似“点播”,每次订阅都会从头开始。

由上述分析,可以得知RAC2和RAC4中的冷热信号有如下关系:

RAC2 RAC4
冷信号 RACSignal SignalProducer
热信号 RACSubject Singal

3. 冷信号的副作用(Side Effect)

细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号提出了,如果冷信号中包含网络请求,那么每次订阅这个冷信号都会发送网络请求,而且任何的信号转换即是对原有的信号进行订阅从而产生新的信号

对于上述遇到的副作用,有两种解决办法:

  1. 确保只对冷信号进行一次订阅
  2. 将冷信号转换成特殊的热信号

对于解决方法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,这种信号特点在于:

  1. 无论是否有订阅者订阅该信号,该信号都会发送事件,这点和热信号一致
  2. 无论订阅者何时订阅信号,订阅者都能立即接收到该信号所发送的事件,这点和冷信号相似,但有很大的不同

而在RAC4中,我们使用SignalProducer.buffer(Int)这个方法来代替RACReplaySubjectUsing SignalProducer.buffer instead of replaying

同样通过一个实验来证明SignalProducer.bufferRACReplaySubject具有同样的特点

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

  1. 创建特殊的热信号,并在1s,4s后发送两个包
  2. 在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. 所有事件,信号只发送了一次
  2. 订阅者1是0s订阅的,毫无疑问,订阅者1可以接收到所有事件
  3. 订阅者2是4s才订阅的,而此时信号已经发出了所有的事件,如果是普通的热信号,订阅者2是接受不到任何事件的,但这里订阅者2却同时接收到了信号发送的所有事件,就像所有的事件缓存起来一样

根据特点,我们可以得到ReplaySubjectSingalProducer.buffer产生的信号的图

特殊的热信号.png

而且这种信号的命名也很有意思,在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吧!只有Subscriber1signal send hello顺序颠倒了,从时间上来看,信号发送事件的时间延迟了。

5. bufferreplayLazily中的参数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,输出结果会有略微的不同,自己分析原因吧

图片引用自:细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号

`

推荐阅读更多精彩内容

  • 前言 关于ReactiveCocoa v2.5中冷信号和热信号的文章中,最著名的就是美团的臧成威老师写的3篇冷热信...
    一缕殇流化隐半边冰霜阅读 6,868评论 7 55
  • RAC使用测试Demo下载:github.com/FuWees/WPRACTestDemo 1.ReactiveC...
    FuWees阅读 5,134评论 3 10
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 131,304评论 18 138
  • RAC的冷信号和热信号 本篇目录 何为冷信号,热信号; 为何会有RACSubject这个类的存在; RACSubj...
    fanglaoda阅读 3,823评论 0 12
  • “。。你看不到病菌,不代表病菌本身没有。。”医生说道。
    U0阅读 194评论 0 1
  • 断断续续一个多月,终于看完了京华烟云。从学生时代,就心心念念却一直未曾静心细读的作品。 据说当年,林语堂本是想英译...
    胡叶平阅读 495评论 0 1
  • 语文老师一枚,和孩子们一起完成的一次写诗尝试,对他们的要求是:想象越有趣越好,再写得好听一点就更好了😊 ...
    小翌阅读 219评论 2 0
  • 一 除了生死,哪一件不是闲事。 二 如果记忆没有差错,我很小的时候养过几条小金鱼,磕磕绊绊地在我的虐待下活了六年多...
    李小耳阅读 118评论 0 1