ReactiveCocoa 2.0 的冷热信号

一直以来对于 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 {
RACSubject *subject = [[RACSubject subject] setNameWithFormat:@"[%@] -publish", self.name];
RACMulticastConnection *connection = [self multicast:subject];
return connection;
}

可以发现,他们都有一个 RACMulticastConnectionsubject。不同的是,前者是一个 RACSubject 的子类 RACReplaySubject,而且它是先调用 connect,再返回持有的 signal,后者则是先返回 connection,然后外部订阅其持有的信号,然后再调用 connect

那么再看看 RACReplaySubjectRACSubject 有什么不同?

- (void)replaySubject{

RACSubject *letters = [RACReplaySubject subject];
RACSignal *signal = letters;

[signal subscribeNext:^(id x) {
NSLog(@"S1: %@", x);
}];

[letters sendNext:@"A"];

NSLog(@"Subscribe S2");
[signal subscribeNext:^(id x) {
NSLog(@"S2: %@", x);
}];

[letters sendNext:@"B"];
}
//输出:
//2015-10-29 20:42:35.246 Demo[15981:2568563] S1: A
//2015-10-29 20:42:35.246 Demo[15981:2568563] Subscribe S2
//2015-10-29 20:42:35.246 Demo[15981:2568563] S2: A
//2015-10-29 20:42:35.247 Demo[15981:2568563] S1: B
//2015-10-29 20:42:35.247 Demo[15981:2568563] S2: B

这里的代码,其实就是刚才热信号 3 的代码,只是我把其中的 RACSubject 换成了 RACReplaySubject

可以发现在 sendNext:@"A” 之后第二个订阅者才进行订阅,不过订阅者仍然收到了 A。说明 RACReplaySubject 其实内部持有一个数组,它会把历史的信号值储存在数组里面,每次有个新的订阅者,它都会把过往的历史值都向它再从新发送一遍。

那么这也就不难解释刚才热信号 1 和 热信号 2 这两个不同的操作方法,最后却可以产生一样的效果。

最后再看看 RACMulticastConnectionconnect

- (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 转化产生新信号的过程中,被重新订阅了,从而产生了副作用,比如多次的网络请求,造成了不必要的开销。关于这方面的更多内容,可查看美团的文章。