ReactiveCocoa使用之细说信号的订阅

前言

我们在使用ReactiveCocoa的时候,对RACSignal以及RACSubject的订阅非常频繁,虽然订阅的代码写起来并不繁琐,但里面也会存在一些比较细的问题,这篇文章主要针对信号在订阅中出现的某些问题进行一系列的分析。

RACSignal的重复订阅

在这里我首先创建一个普通的信号:

RACSignal *signalOne = [RACSignal createSignal:^RACDisposable *(id subscriber) {
        static NSInteger flag = 0;
        [subscriber sendNext:@(flag++)];
        return nil;
    }];

从这个信号的构建中我们可以看出,当程序每次执行这个信号的构建block时,里面设置的静态整数标识符都会被以信号事件的形式发射出去,并且每次被发射,它都会进行自增。

下面我们来对这个信号进行重复订阅:

[signalOne subscribeNext:^(NSNumber *x) {
    NSLog(@"Subscriber1--%ld",[x integerValue]);
}];
    
[signalOne subscribeNext:^(NSNumber *x) {
    NSLog(@"Subscriber2--%ld",[x integerValue]);
}];
    
[signalOne subscribeNext:^(NSNumber *x) {
    NSLog(@"Subscriber3--%ld",[x integerValue]);
}];

在这里我创建了三个订阅者来对这一个事件进行重复订阅,并且在订阅block中进行输入事件的打印。

跑起来,让我们看看控制台输出了什么:

Subscriber1--0
Subscriber2--1
Subscriber3--2

可见,当我们每次对这个信号进行订阅时,信号的创建block都会执行一次。

如果这时候我有一个需求:不管有多少个订阅者,信号的构建block都只执行一次,订阅的事件都是这次构建block所发送的事件。我该怎么做?

这里我可以使用两种方法:

RACMulticastConnection

使用方法:

  1. 在这里我创建一个信号,它里面的构建block跟上面的signalOne一样:
RACSignal *signalTwo = [RACSignal createSignal:^RACDisposable *(id subscriber) {
       static NSInteger flag = 0;
       [subscriber sendNext:@(flag++)];
       return nil;
   }];
  1. 以新创建的信号为基础,创建一个RACMulticastConnection
RACMulticastConnection *connection = [signalTwo publish];
  1. 利用新创建的RACMulticastConnection取出其信号属性进行订阅:
RACSignal *connectionSignal = connection.signal;
   
   [connectionSignal subscribeNext:^(NSNumber *x) {
       NSLog(@"Subscriber1--%ld",[x integerValue]);
   }];
   
   [connectionSignal subscribeNext:^(NSNumber *x) {
       NSLog(@"Subscriber2--%ld",[x integerValue]);
   }];
   
   [connectionSignal subscribeNext:^(NSNumber *x) {
       NSLog(@"Subscriber3--%ld",[x integerValue]);
   }];
  1. 开始连接:
[connection connect];

注意最后一步不能够漏掉,仅仅实现了之前的三步并不会让connectionSignal发送事件,因为当你执行connect方法时,connection才会去订阅源信号,所以connectionSignal实质上其实是一个RACSubject,当源信号发送事件时,相应的connectionSignal会调用sendNext:

现在运行程序,看看控制台打印出什么结果:

Subscriber1--0
Subscriber2--0
Subscriber3--0

可见,运用此方法,源信号signalTwo的创建block只执行了一次。


Replay、ReplayLast、ReplayLazily

这三个是RACSignal都可以使用的方法,它们的返回值也是一个信号,并且对于RACSignalreplayreplayLazily的效果一样,所以接下来我主要讲解replayreplayLast相关使用以及区别:

  1. 创建两个信号,它们的构建block与前面不同的是里面会再发送一次带有flag的事件:
RACSignal *signalThree = [RACSignal createSignal:^RACDisposable *(id subscriber) {
       static NSInteger flag = 0;
       [subscriber sendNext:@(flag++)];
       [subscriber sendNext:@(flag++)];
       return nil;
   }];
RACSignal *signalFour =[RACSignal createSignal:^RACDisposable *(id subscriber) {
       static NSInteger flag = 0;
       [subscriber sendNext:@(flag++)];
       [subscriber sendNext:@(flag++)];
       return nil;
   }];
  1. 基于前面创建的两个信号,利用replay以及replayLast分别再引出两个信号signalFivesignalSix
RACSignal *signalFive = [signalThree replay];
   RACSignal *signalSix = [signalFour replayLast];
  1. 分别对signalFivesignalSix进行两次订阅:
   [signalFive subscribeNext:^(NSNumber *x) {
       NSLog(@"Subscriber1-1---%ld",[x integerValue]);
   }];
   [signalFive subscribeNext:^(NSNumber *x) {
       NSLog(@"Subscriber1-2---%ld",[x integerValue]);
   }];
   //  --------------------------------------
   [signalSix subscribeNext:^(NSNumber *x) {
       NSLog(@"Subscriber2-1---%ld",[x integerValue]);
   }];
   [signalSix subscribeNext:^(id x) {
       NSLog(@"Subscriber2-2---%ld",[x integerValue]);
   }];
   ```
4.  运行程序,看看控制台的输出:

Subscriber1-1---0
Subscriber1-1---1
Subscriber1-2---0
Subscriber1-2---1


Subscriber2-1---1
Subscriber2-2---1


由此可得结论:

- 当我们使用`replay`处理信号后,不管订阅者有多少个,信号的构建block都只会执行一次,且信号的构建block里若有N次事件发送,订阅者就会接收到N次的事件。
- 当我们使用`replayLast`处理信号后,不管订阅者有多少个,信号的构建block都只会执行一次,其不管信号的构建block里有多少次事件发送,订阅者都只会接收到信号最后一次发送的事件。

## RACSubject的信号重接收订阅
对于`RACSubject`的订阅来说,其有两种订阅顺序:

- 对subject信号的订阅在其`sendNext:`之前
- 对subject信号的订阅在其`sendNext:`之后

对于第一种情况,若订阅者先订阅了subject,在之后subject才去发送事件,这样子的话所有订阅者都会接收到此subject发送的全部事件,所以对于这种情况我们不进行讨论。

我们现在针对第二种情况对某个`RACSubject`进行普通的订阅:

1. 在这里我们创建一个`RACSubject`,并让他先发送三个事件,然后我们对它进行订阅:

 ```objc
RACSubject *subjectOne = [RACSubject subject];
[subjectOne sendNext:@(0)];
[subjectOne subscribeNext:^(NSNumber *x) {
    NSLog(@"Subscribe--%ld",[x integerValue]);
}];
  1. 让我们让程序跑起来,我们会看到控制台并无打印。

可见,对于RACSubject,若我们在其发送事件之后进行普通的订阅,订阅者并不会收到它发送的事件。

若我们想做到在RACSubject发送事件后进行订阅,且又能接收到它在被订阅前发送的事件,我们应该怎么办呢?

这时候也是有两个方法供君选择:

ReplayReplayLastReplayLazily

在文章前面我也说到过这三个方法也可用于RACSignal的重复订阅,其实在RACSubject中这三个方法的效果跟RACSignal也非常相似,现在就让我们来看一下:
首先,我们先对replayreplayLast进行分析:

  1. 创建一个RACSubject,利用replayreplayLast从它里面引出两个信号,并让它发送两个带有整数的事件:
   RACSubject *subjectTwo = [RACSubject subject];
   RACSignal *replaySignal = subjectTwo.replay;
   RACSignal *replayLastSignal = subjectTwo.replayLast;
   [subjectTwo sendNext:@(0)];
   [subjectTwo sendNext:@(1)];

注意,利用replayreplayLast引出信号的这两个语句不能够写在subject发送事件之后,不然的话订阅者就无法接收到那些事件了。

  1. 分别订阅两个引出信号:
[replaySignal subscribeNext:^(NSNumber *x) {
       NSLog(@"One--%ld",[x integerValue]);
   }];
   [replayLastSignal subscribeNext:^(NSNumber *x) {
       NSLog(@"Two--%ld",[x integerValue]);
   }];
  1. 现在,我们运行程序,看看控制台输出了什么:
One--0
One--1
---------
Two--1

由此可见:

  • 当我们订阅被replay进行处理的RACSubject后,订阅者会收到它在订阅前RACSubject发送的全部事件。
  • 当我们订阅被replayLast进行处理的RACSubject后,订阅者会收到它在订阅前RACSubject最后一次发送的事件。

现在我们再看看replayLazily

其实replayLazilyreplay是非常相似的,都可以让订阅者接收到RACSubject被订阅前发送的所有事件,但是,replayLazily比起后者多出了"Lazily",我们可从它的命名知道它是具有"懒惰性"的,现在我就通过一个例子来对比一下replayLazilyreplay

  1. 我们先创建一个RACSubject,利用replayreplayLazily从它里面引出两个信号,并让它发送一个带有整数"0"的事件:
RACSubject *subjectThree = [RACSubject subject];
   RACSignal *replaySignal = subjectThree.replay;
   RACSignal *replayLazilySignal = subjectThree.replayLazily;
   [subjectThree sendNext:@(0)];
  1. 然后我们分别对两个引出信号进行订阅:
[replaySignal subscribeNext:^(NSNumber *x) {
       NSLog(@"One--%ld",[x integerValue]);
   }];
   [replayLazilySignal subscribeNext:^(NSNumber *x) {
       NSLog(@"Two--%ld",[x integerValue]);
   }];
  1. 现在我们运行程序,查看控制台的打印:
One--0

可见,与replay相比,使用replayLazily引出的信号在订阅后没有接收到在其订阅前RACSubject发送的事件。

现在我们将代码改一改,在subject发送事件的前面再插一个订阅的语句,最终就是这样子:

RACSubject *subjectThree = [RACSubject subject];
RACSignal *replaySignal = subjectThree.replay;
RACSignal *replayLazilySignal = subjectThree.replayLazily;
[replayLazilySignal subscribeNext:^(NSNumber *x) {
    NSLog(@"Zero--%ld",[x integerValue]);
}];
[subjectThree sendNext:@(0)];

其他的代码我们不需要修改,然后现在我们运行一下程序,看看控制台的输出:

Zero--0
One--0
Two--0

现在我们可以看到,事件发送前后的replayLazily信号都可以接收到事件,由此我们可以得出结论:

利用replayLazilyRACSubject里引出的信号,存在"懒惰性",这个信号开始可以理解为一个"冷信号",需要在RACSubject发送事件前先被订阅一次,让其激活成为"热信号",然后才可以在RACSubject发送事件后再进行订阅,这样子就能够接收到订阅前subject发送的信号。


RACReplaySubject

RACReplaySubject可以充当上面的replay以及replayLast方法,接收订阅前发送的全部事件或者最后一个事件,不仅如此,你还能设置让它接收特定数量的事件,下面我就展示一下它的使用:

  1. 我们用另一种构建方法构建一个RACReplaySubject,通过设置capacity来限定它接收重接收事件的数量,并让它发送三个事件:
   RACReplaySubject *replaySubject = [RACReplaySubject replaySubjectWithCapacity:2];
                                     //  [RACReplaySubject subject];
   [replaySubject sendNext:@(0)];
   [replaySubject sendNext:@(1)];
   [replaySubject sendNext:@(3)];
  1. 订阅replaySubject:
[replaySubject subscribeNext:^(NSNumber *x) {
       NSLog(@"Subscriber--%ld",[x integerValue]);
   }];
  1. 运行程序,查看控制台打印:
Subscriber--1
Subscriber--3

可见,RACReplaySubject会接收最新的两条事件。

参考资料

Comparing replay, replayLast, and replayLazily

最快让你上手ReactiveCocoa之基础篇

你可能感兴趣的:(ReactiveCocoa使用之细说信号的订阅)