一直以来对于 RAC 2.0 中冷信号与热信号的区别了解得并不多。一方面,它们都属于 RACSignal 这个类,刚入门之时往往不太注意它们之间的区别,早期的一些文章对于这方面的阐述并不多,往往只是几句话带过。另一方面,自己也没有太重度使用这个库。
到了 RAC 3.0 时代(4.0也有了@_@),RACSignal
信号被拆成2个独立的类,Signal
(热)和 SignalProducer
(冷)。对于它们之间的区别也开始有了好奇。最近也有文章对这方面进行了深入的探讨:细说 ReactiveCocoa 的冷信号与热信号(一)和(二),写得很详细,未来貌似还有新的文章会继续探讨。
- RAC 2.0:RACSignal
- RAC 3.0:Signal(热) 和 SignalProducer(冷)
简介
本文主要探讨 RAC 2.0 冷热信号之间的区别,关于它们的定义,我们不如先从 3.0 的文档着手。毕竟核心理念是一致的。
SIGNAL(热信号)
- Signals start work when instantiated
- Observing a signal does not have side effects
- All observers of a signal see the same events in the same order
SIGNALPRODUCER(冷信号)
- Signal producers start work on demand by creating signals
- Each produced signal may send different events at different times
大致翻译过来,可以理解为:
热信号:主动的,多个订阅者收到的是同一个信号,没有副作用(side effect)。
冷信号:被动的,多个订阅者收到的信号是不同的信号,一般有副作用。
代码演示
那么在 RAC 2.0 里面,冷信号和热信号到底长什么样呢?
冷信号:
- (void)codeSignal{
__block int num = 0;
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id subscriber) {
num++;
NSLog(@"Increment num to: %i", num);
[subscriber sendNext:@(num)];
return nil;
}];
NSLog(@"Subscribe S1");
[signal subscribeNext:^(id x) {
NSLog(@"S1: %@", x);
}];
NSLog(@"Subscribe S2");
[signal subscribeNext:^(id x) {
NSLog(@"S2: %@", x);
}];
}
//输出:
//2015-10-29 20:02:29.273 Demo[15695:2428799] Subscribe S1
//2015-10-29 20:02:29.276 Demo[15695:2428799] Increment num to: 1
//2015-10-29 20:02:29.276 Demo[15695:2428799] S1: 1
//2015-10-29 20:02:29.276 Demo[15695:2428799] Subscribe S2
//2015-10-29 20:02:29.276 Demo[15695:2428799] Increment num to: 2
//2015-10-29 20:02:29.276 Demo[15695:2428799] S2: 2
从输出的信息可以看到,
- block 引用了外部变量 num,每次输出会递增,也就是所谓的副作用;
- “Subscribe S1” 在 block “Increment num to: 1” 之前执行,说明是被动的;
- “S1: 1” 与 “S2: 2” 说明 2 个订阅者收到了不同的信号。
下面我们再来看看热信号,demo 比较多,
热信号 1:- (void)replay{
__block int num = 0;
RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id subscriber) {
num++;
NSLog(@"Increment num to: %i", num);
[subscriber sendNext:@(num)];
return nil;
}] replay];
NSLog(@"Start subscriptions");
[signal subscribeNext:^(id x) {
NSLog(@"S1: %@", x);
}];
[signal subscribeNext:^(id x) {
NSLog(@"S2: %@", x);
}];
[signal subscribeNext:^(id x) {
NSLog(@"S3: %@", x);
}];
}
//输出
//2015-10-29 19:49:47.880 Demo[15459:2381206] Increment num to: 1
//2015-10-29 19:49:47.881 Demo[15459:2381206] Start subscriptions
//2015-10-29 19:49:47.882 Demo[15459:2381206] S1: 1
//2015-10-29 19:49:47.882 Demo[15459:2381206] S2: 1
//2015-10-29 19:49:47.882 Demo[15459:2381206] S3: 1
从输出的信息可以看到:它满足热信号的特征:主动,多个订阅者收到的是同一个信号,没有副作用。这里不再详细描述,以下也是热信号的例子。
热信号 2:- (void)multicastConnection{
__block int num = 0;
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id subscriber) {
num++;
NSLog(@"Increment num to: %i", num);
[subscriber sendNext:@(num)];
return nil;
}];
RACMulticastConnection *connection = [signal publish];
[connection.signal subscribeNext:^(id x) {
NSLog(@"S1: %@", x);
}];
[connection.signal subscribeNext:^(id x) {
NSLog(@"S2: %@", x);
}];
[connection.signal subscribeNext:^(id x) {
NSLog(@"S3: %@", x);
}];
NSLog(@"start connect");
[connection connect];
}
//输出:
//2015-10-29 21:00:04.889 Demo[16167:2631590] start connect
//2015-10-29 21:00:04.891 Demo[16167:2631590] Increment num to: 1
//2015-10-29 21:00:04.891 Demo[16167:2631590] S1: 1
//2015-10-29 21:00:04.891 Demo[16167:2631590] S2: 1
//2015-10-29 21:00:04.891 Demo[16167:2631590] S3: 1
RACMulticastConnection
有些文章把它翻译为组播,可以把它理解为广播,或者通知。
热信号 3:- (void)subject{
RACSubject *letters = [RACSubject subject];
RACSignal *signal = letters;
[signal subscribeNext:^(id x) {
NSLog(@"S1: %@", x);
}];
[letters sendNext:@"A"];
[signal subscribeNext:^(id x) {
NSLog(@"S2: %@", x);
}];
NSLog(@"Subscribe S2");
[letters sendNext:@"B"];
}
//输出
//2015-10-29 20:07:17.761 Demo[15771:2453222] S1: A
//2015-10-29 20:07:17.761 Demo[15771:2453222] Subscribe S2
//2015-10-29 20:07:17.761 Demo[15771:2453222] S1: B
//2015-10-29 20:07:17.761 Demo[15771:2453222] S2: B
RACSubject
是一个 RACSignal
的子类,它本身既是信号,同时又是信号的发送者,可以主动触发产生的新的信号。常会用于 ViewModel 中,外部控制器进行订阅,触发刷新 tableView。而其内部持有一个数组,会储存每个订阅者,每次产生新的信号都会遍历每个订阅者,向它们发送信号。
分析
热信号的表现形式貌似有点多,其本质是什么呢?
不如先看看热信号 1 的 replay
和热信号 2 的publish
的是啥?- (RACSignal *)replay {
RACReplaySubject *subject = [[RACReplaySubject subject] setNameWithFormat:@"[%@] -replay", self.name];
RACMulticastConnection *connection = [self multicast:subject];
[connection connect];
return connection.signal;
}
- (RACMulticastConnection *)publish { |
可以发现,他们都有一个 RACMulticastConnection
和 subject
。不同的是,前者是一个 RACSubject
的子类 RACReplaySubject
,而且它是先调用 connect
,再返回持有的 signal,后者则是先返回 connection,然后外部订阅其持有的信号,然后再调用 connect
。
那么再看看 RACReplaySubject
和 RACSubject
有什么不同?
- (void)replaySubject{ |
这里的代码,其实就是刚才热信号 3 的代码,只是我把其中的 RACSubject
换成了 RACReplaySubject
。
可以发现在 sendNext:@"A”
之后第二个订阅者才进行订阅,不过订阅者仍然收到了 A。说明 RACReplaySubject
其实内部持有一个数组,它会把历史的信号值储存在数组里面,每次有个新的订阅者,它都会把过往的历史值都向它再从新发送一遍。
那么这也就不难解释刚才热信号 1 和 热信号 2 这两个不同的操作方法,最后却可以产生一样的效果。
最后再看看 RACMulticastConnection
的 connect
:- (id)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject {
self = [super init];
if (self == nil) return nil;
_sourceSignal = source;
_serialDisposable = [[RACSerialDisposable alloc] init];
_signal = subject;
return self;
}
- (RACDisposable *)connect {
BOOL shouldConnect = OSAtomicCompareAndSwap32Barrier(0, 1, &_hasConnected);
if (shouldConnect) {
self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal];
}
return self.serialDisposable;
}
里面的 [self.sourceSignal subscribe:_signal]
和冷信号调用的方法是一样的,只是 _signal
它是 RACSubject
类型,但是也遵从了 RACSubscriber
协议。- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber;
总结
所以冷信号和热信号虽然都是属于 RACSignal
,但是它们的 Subscriber 并不同,前者是 RACSubscriber
,而后者是 RACSubject
及其子类,会遍历所有的订阅者发送同一个信号,而对于持有的 block 也只会调用一次。多组合,少继承,我觉得 <RACSubscriber>
是一个很好的例子。
在实际的使用过程中,我们往往还是冷信号为主,热信号为辅。冷信号适合被用于执行任务,网络请求等,而热信号更适合处理 UI 事件流。
同时,我们也应注意避免冷信号所带来的一些不必要的影响。比如信号在使用 flattenMap
转化产生新信号的过程中,被重新订阅了,从而产生了副作用,比如多次的网络请求,造成了不必要的开销。关于这方面的更多内容,可查看美团的文章。