前言

本文主旨是浅显易懂的讲解下冷热信号的区别和常见的使用误区,所以篇幅所限不会介绍些内部细节。

如果想要了解的更深入,可以参照William Zang的博文:细说ReactiveCocoa的冷信号与热信号

文中部分内容参考RAC 4.x的文档:设计指南框架概览,但是文章本身是介绍ObjC的RAC 2.5。

另外和前一篇博文一样,我称呼一组信号叫做信号流,单次发送的信号值为信号

热信号流

热信号流在RAC 3.x以后为Signal,在RAC 2.5中对应RACSubject。

特征

所谓热信号流是用来观察一组随时间流逝的事件用的。一般来说它被用作观察执行中(in progress)事件的进度或流程,例如下载事件的进度和流程。

热信号流有以下特征:

  1. 随信号发出者自己的进度发送信号,创建后即开始工作。
  2. 新增观察者对信号流不产生任何副作用,无论有没有观察者这个信号流都做相同的工作。
  3. 所有观察者都会在同样的时间收到同样的信号。

例子

举个例子,蒸包子大师傅一天的工作就可以成为一个热信号流,而围观大师傅蒸包子的人就是观察者:

  1. 每半小时出笼一次包子,算作发送一次信号。大师傅兢兢业业,早上上班后就开始一直蒸包子。
  2. 有人来围观大师傅蒸包子对大师傅不产生影响,无论有多少人围观大师傅,大师傅都是半小时出笼一次。
  3. 每隔半小时,正在围观的人都能同时看到大师傅出笼一次包子。错过的人就看不到这次的出笼事件。

可以说,这里的围观者更关注的是大师傅什么时候出笼包子这类事件。

代码

注意我们先在RACSubject.m里加上一段NSLog来记录发出的事件:

1
2
3
4
- (void)sendNext:(id)value {
NSLog(@"RACSubject sendNext: %@", value);
...
}

然后写代码模仿上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NSLog(@"create RACSubject");

RACSubject *subject = [RACSubject subject];
[subject sendNext:@"大师傅上班"];
[[RACScheduler mainThreadScheduler] afterDelay:1.0 schedule:^{
[subject sendNext:@"第一次出笼"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:2.0 schedule:^{
[subject sendNext:@"第二次出笼"];
}];

[[RACScheduler mainThreadScheduler] afterDelay:0.5 schedule:^{
NSLog(@"路人甲开始围观");
[subject subscribeNext:^(NSString *msg) {
NSLog(@"路人甲看到%@", msg);
}];
}];

[[RACScheduler mainThreadScheduler] afterDelay:1.5 schedule:^{
NSLog(@"路人乙开始围观");
[subject subscribeNext:^(NSString *msg) {
NSLog(@"路人乙看到%@", msg);
}];
}];

观察日志:

1
2
3
4
5
6
7
8
9
2017-09-21 15:26:52.657 MvvmDemo[1246:146676] create RACSubject
2017-09-21 15:26:52.658 MvvmDemo[1246:146676] RACSubject sendNext: 大师傅上班
2017-09-21 15:26:53.203 MvvmDemo[1246:146676] 路人甲开始围观
2017-09-21 15:26:53.658 MvvmDemo[1246:146676] RACSubject sendNext: 第一次出笼
2017-09-21 15:26:53.659 MvvmDemo[1246:146676] 路人甲看到第一次出笼
2017-09-21 15:26:54.308 MvvmDemo[1246:146676] 路人乙开始围观
2017-09-21 15:26:54.856 MvvmDemo[1246:146676] RACSubject sendNext: 第二次出笼
2017-09-21 15:26:54.856 MvvmDemo[1246:146676] 路人甲看到第二次出笼
2017-09-21 15:26:54.857 MvvmDemo[1246:146676] 路人乙看到第二次出笼

可以看到这完美的诠释了以上热信号流的各种特征。

冷信号流

冷信号流在RAC 3.x以后叫做Signal Producers,在RAC 2.5中对应RACSignal。

注意RACSubject是RACSignal的一个子类,这也是RAC中大家常搞不清冷热信号流的一个重要原因。😂

特征

所谓冷信号流是创建一组信号和处理副作用的。它的主要作用是处理一组完整的操作或任务,供用户观察事件从开始到结束的整个流程和事件的结果。例如处理一整段网络请求相关操作,供用户观察请求的结果是什么。

冷信号流有以下特征:

  1. 创建时并不立刻开始工作,在被订阅的时候才开始工作。就是说订阅操作本身会对信号流产生作用。
  2. 每个新增的订阅者收到的可能都是不同的,分别独立的信号流。这些信号流的结果也可能不同,因为冷信号流可能包含一些副作用

例子

举个例子,包子店收银员卖出包子的过程就可以作为一个冷信号流,而每个买包子的顾客就是一个订阅者:

  1. 收银员上班的时候不是立刻就有工作,在一个新的顾客来的时候才会进行卖包子的工作。
  2. 每个顾客买到的包子都是不同的包子(即使馅一样,也是不同的实例)。而因为还有找不开钱,包子卖完了等各种副作用存在,有可能顾客还会买不到包子。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
NSLog(@"create RACSignal");

static NSInteger count = 2;
RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
if (count > 0) {
[subscriber sendNext:@"给收银员钱"];
[subscriber sendNext:@"买到包子"];
} else {
[subscriber sendNext:@"没买到包子"];
}
count--;
[subscriber sendCompleted];
return nil;
}];

[[RACScheduler mainThreadScheduler] afterDelay:1.0 schedule:^{
[signal subscribeNext:^(NSString *msg) {
NSLog(@"顾客甲%@", msg);
}];
}];

[[RACScheduler mainThreadScheduler] afterDelay:2.0 schedule:^{
[signal subscribeNext:^(NSString *msg) {
NSLog(@"顾客乙%@", msg);
}];
}];

[[RACScheduler mainThreadScheduler] afterDelay:3.0 schedule:^{
[signal subscribeNext:^(NSString *msg) {
NSLog(@"顾客丙%@", msg);
}];
}];

注意,这里我们用count模拟了一个剩余包子数量的外部值作为副作用。但是这里没有做多线程保护,各位自己写代码的时候一定要记得做好对应的多线程保护。

另外要加个NSLog,RACSignal发送信号其实最终是在RACPassthroughSubscriber.m里做的:

1
2
3
4
5
- (void)sendNext:(id)value {
...
NSLog(@"RACSignal sendNext: %@", value);
[self.innerSubscriber sendNext:value];
}

观察日志:

1
2
3
4
5
6
7
8
9
10
11
2017-09-21 17:09:06.349 MvvmDemo[1494:203071] create RACSignal
2017-09-21 17:09:07.351 MvvmDemo[1494:203071] RACSignal sendNext: 给收银员钱
2017-09-21 17:09:07.351 MvvmDemo[1494:203071] 顾客甲给收银员钱
2017-09-21 17:09:07.351 MvvmDemo[1494:203071] RACSignal sendNext: 买到包子
2017-09-21 17:09:07.351 MvvmDemo[1494:203071] 顾客甲买到包子
2017-09-21 17:09:08.350 MvvmDemo[1494:203071] RACSignal sendNext: 给收银员钱
2017-09-21 17:09:08.351 MvvmDemo[1494:203071] 顾客乙给收银员钱
2017-09-21 17:09:08.351 MvvmDemo[1494:203071] RACSignal sendNext: 买到包子
2017-09-21 17:09:08.351 MvvmDemo[1494:203071] 顾客乙买到包子
2017-09-21 17:09:09.648 MvvmDemo[1494:203071] RACSignal sendNext: 没买到包子
2017-09-21 17:09:09.648 MvvmDemo[1494:203071] 顾客丙没买到包子

可以看到一开始创建RACSignal的时候没有触发买包子流程,每有一个新的顾客来就会触发一个新的流程,流程也可能走向不同的结果。

误用的隐患

冷信号流容易导致的问题

冷信号流是用于一对一环境的,为每个订阅者都重新执行一次独立的信号流。因此,冷信号流可能无意中被订阅很多次,重复运行很多次。看下面的代码:

1
2
3
4
5
6
7
8
9
10
RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
NSLog(@"触发一次信号流");
[subscriber sendCompleted];
return nil;
}];

[[signal flatten] subscribeNext:^(id _Nullable x) { }];
[[signal map:^id _Nullable(id _Nullable value) {
return nil;
}] subscribeNext:^(id _Nullable x) { }];

这例子里的RACSignal的flattenmap:等方法其实也是通过订阅原信号流进行转换后再输出的,所以这里是会打印出两次『触发一次信号流』的。这种情况的增多,就更容易导致冷信号流被订阅次数增加。

在这种前提下,我们就要注意以下情况下不能滥用冷信号流:

  1. 信号体内有耗时的运算逻辑,如多次运行会造成性能问题。
  2. 信号体内逻辑实际上不期望多次运行,如多次运行会出错。

这时候我们可以用热信号流替代冷信号流,或者将冷信号流通过publish或者multicast:转换成RACMulticastConnection后进行热信号流式的一对多转发。这细节就不在此展开了,只给个示例:

1
2
3
4
5
6
7
8
9
10
11
12
RACMulticastConnection *connection = [[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
NSLog(@"触发一次信号流");
[subscriber sendCompleted];
return nil;
}] publish];

[[connection.signal flatten] subscribeNext:^(id _Nullable x) { }];
[[connection.signal map:^id _Nullable(id _Nullable value) {
return nil;
}] subscribeNext:^(id _Nullable x) { }];

[connection connect];

热信号流容易导致的问题

热信号流的问题就在于后订阅的订阅者可能丢失之前的某些信号,如果确实需要之前的某些信号时,可以使用RACBehaviorSubject或RACReplaySubject来完成。当然要注意的是,使用这两个类的时候,就只能关注信号的值而不能关心信号发生的时间了,因为时间在replay过程中已经被破坏了。

总结

总结起来,热信号流就像直播视频,冷信号流就像点播视频。正确的使用它们,是用好RAC的基础。

大家加油,有问题的欢迎共同讨论哦。