前言
本文主旨是浅显易懂的讲解下冷热信号的区别和常见的使用误区,所以篇幅所限不会介绍些内部细节。
如果想要了解的更深入,可以参照William Zang的博文:细说ReactiveCocoa的冷信号与热信号。
文中部分内容参考RAC 4.x的文档:设计指南、框架概览,但是文章本身是介绍ObjC的RAC 2.5。
另外和前一篇博文一样,我称呼一组信号叫做信号流,单次发送的信号值为信号。
热信号流
热信号流在RAC 3.x以后为Signal,在RAC 2.5中对应RACSubject。
特征
所谓热信号流是用来观察一组随时间流逝的事件用的。一般来说它被用作观察执行中(in progress)事件的进度或流程,例如下载事件的进度和流程。
热信号流有以下特征:
- 随信号发出者自己的进度发送信号,创建后即开始工作。
- 新增观察者对信号流不产生任何副作用,无论有没有观察者这个信号流都做相同的工作。
- 所有观察者都会在同样的时间收到同样的信号。
例子
举个例子,蒸包子大师傅一天的工作就可以成为一个热信号流,而围观大师傅蒸包子的人就是观察者:
- 每半小时出笼一次包子,算作发送一次信号。大师傅兢兢业业,早上上班后就开始一直蒸包子。
- 有人来围观大师傅蒸包子对大师傅不产生影响,无论有多少人围观大师傅,大师傅都是半小时出笼一次。
- 每隔半小时,正在围观的人都能同时看到大师傅出笼一次包子。错过的人就看不到这次的出笼事件。
可以说,这里的围观者更关注的是大师傅什么时候出笼包子这类事件。
代码
注意我们先在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 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的flatten
和map:
等方法其实也是通过订阅原信号流进行转换后再输出的,所以这里是会打印出两次『触发一次信号流』的。这种情况的增多,就更容易导致冷信号流被订阅次数增加。
在这种前提下,我们就要注意以下情况下不能滥用冷信号流:
- 信号体内有耗时的运算逻辑,如多次运行会造成性能问题。
- 信号体内逻辑实际上不期望多次运行,如多次运行会出错。
这时候我们可以用热信号流替代冷信号流,或者将冷信号流通过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的基础。
大家加油,有问题的欢迎共同讨论哦。