ReactiveCocoa学习笔记(四):「RAC微博」基础使用手册

本文简单介绍了ReactiveCocoa的基础用法,希望读完能对这个框架的使用有一个大概的了解。

前面几篇文章,我们研究了ReactiveCocoa(以下简称RAC)的起源和思想。网络上介绍RAC的使用的好文甚多,在文末的Reference中标出了一些以供参考。RAC这个开源库本身也是十分良心,代码注释十分齐全,因此这一篇文章不做太多的扩展,仅谈一谈RAC的基础使用方法。

RACSignal

上文中说到,函数响应式编程中,将各种通信机制所需要解决的「输入」与「输出」的异步关系抽象成了事件/时间驱动的值流,并通过monad使其支持了函数式编程的特性。而在RAC中,这个东西就是RACStream,开发过程中我们并不是直接使用它,而是其子类——RACSignalRACSequence。这一节,讲讲RACSignal

Signal,顾名思义,代表一个信号,可以源源不断地给你传递信息。这样就好理解RACSignal代表着「随时间变化的值流」,这里的值,就包含了将来即将到来的「输入」。打个比方,一个微博博主便是一个「Signal」,只要没被封号,你就会知道将来他会一直发出消息。如果关注了这个博主,一旦他开始发消息,新消息会被自动推送到你的设备,因此说RACSignal是一个Push-Driven的值流。

那么,RACSignal博主会发出什么消息呢?一个RACSignal传递的值分为三类:

  1. Next。「Next」代表着一个新的值,一条新的微博。只要这个博主是活跃的,他就会源源不断地发微博。
  2. Error。「Error」则代表着这个Signal出了什么问题,发出了一个代表「错误」的信号。发送出「Error」也就意味着这个Signal的消息到此为止了。比如这位博主被封号了,他就会给你发一条微博,上面写着「404Error」,你就知道他再也不会发微博了……
  3. Completed。代表一个Signal完成了自己的全部信息发送。比如某天这个博主想退出微博了,于是发出最后一条微博——「ByeBye粉丝们」。这就是「Completed」。

一个Signal的信息流,都是由若干个「Next」,加上一个代表终结的「Error」或「Completed」组成的。

这些值都是从哪里来的呢?一个Signal所发出的信息主要来源有两种:

  1. 手动创建一个信号时定义它发出的信息。这就好像是一位原创博主,每条微博都是他自己写的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 代码1
    RACSignal *blogSignalA =
    [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
    [subscriber sendNext:@"blog1"];
    [subscriber sendNext:@"blog2"];
    [subscriber sendNext:@"blog3"];
    [subscriber sendCompleted];
    return nil;
    }];
  2. 由其他通信机制生成一个信号时,在其他通信机制产生输入时发出消息。比如某些大V博主的微博就是专门从杂志、知乎等其他信息载体上将信息搬运过来。RAC提供了很多有力的工具,让我们从传统的Cocoa通信机制中制造出一个信号来:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 代码2
    // signal from KVO
    RACSignal *blogSignalA = RACObserve(someNewspaper, news);
    // signal from UIControl events
    RACSignal *blogSignalB = [someButton rac_signalForControlEvents:UIControlEventTouchUpInside];
    // signal from selectors
    RACSignal *blogSignalB = [self rac_signalForSelector:@selector(viewWillAppear:)];

好了,现在有一个博主能够发出很多消息。但如果没有人关注他,这些信息也不会有多大的作用。对于一个Signal也是一样,创建不是目的,获取它发出的信息才是我们所需要的。在RAC中,这种行为叫「订阅」(Subscribe)。例如,我们想在收到消息时,把消息打印出来,或者做一些其他的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 代码3
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"blog1"];
[subscriber sendNext:@"blog2"];
[subscriber sendNext:@"blog3"];
[subscriber sendCompleted];
return nil;
}];
//subscribe to blogSignalA
[blogSignalA subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
//do something else
} error:^(NSError * _Nullable error) {
NSLog(@"%@",error);
} completed:^{
NSLog(@"Complete!");
}];

现在,我们就关注了blogSignalA这位博主,他发出的blog1blog2等等微博都会推送到我们,由我们进行处理。RAC对于信号的「订阅者」是有要求的,它必须实现了RACSubscriber协议:

1
2
3
4
5
6
7
8
// 代码4
@protocol RACSubscriber <NSObject>
@required
- (void)sendNext:(id)value;
- (void)sendError:(NSError *)error;
- (void)sendCompleted;
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
@end

这也很好理解,因为「订阅者」至少得知道自己需要用这些订阅的值来做什么。上面的代码3中的subscribeNext:error:completed:其实就是帮我们创建了一个内部的「订阅者」,这些在后续如果深入探究源码的时候会详细说明。

此外,Signal支持各种函数式的操作,例如mapreducefilter等等。这可以让我们方便地对原始信号传输出的信息进行一步步加工,最终得到我们所需要的值,这就是「函数性」赋予的利器:

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
// 代码5
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"Sunday"];
[subscriber sendNext:@"Monday"];
[subscriber sendNext:@"Tuesday"];
[subscriber sendNext:@"Wednesday"];
[subscriber sendNext:@"Thursday"];
[subscriber sendNext:@"Friday"];
[subscriber sendNext:@"Saturday"];
[subscriber sendCompleted];
return nil;
}];
RACSignal *blogSignalB =
[blogSignalA map:^id _Nullable(NSString * _Nullable value) {
if ([value isEqualToString:@"Sunday"] || [value isEqualToString:@"Saturday"]) {
return @"Weekend";
}else {
return @"Workday";
}
}];
RACSignal *blogSignalC =
[blogSignalB filter:^BOOL(NSString * _Nullable value) {
return [value isEqualToString:@"Weekend"];
}];
[blogSignalC subscribeNext:^(id _Nullable x) {
NSLog(@"Wow Weekend! Time to Relax!");
}];

这里博主A是一个报时的微博,而博主B是一个翻译的微博,它将A发出的微博进行加工,然后发出「Weekend」和「Workday」两种微博。博主C负责过滤B发出的微博,屏蔽了所有工作日的消息(Nice)。最后我们关注博主C,就能在收到消息推送的时候知道该出去玩啦!当然,有了Monad的保证,我们也可以采用链式语法这么写:

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
// 代码6
RACSignal *blogSignalD =
[[[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"Sunday"];
[subscriber sendNext:@"Monday"];
[subscriber sendNext:@"Tuesday"];
[subscriber sendNext:@"Wednesday"];
[subscriber sendNext:@"Thursday"];
[subscriber sendNext:@"Friday"];
[subscriber sendNext:@"Saturday"];
[subscriber sendCompleted];
return nil;
}] map:^id _Nullable(NSString * _Nullable value) {
if ([value isEqualToString:@"Sunday"] || [value isEqualToString:@"Saturday"]) {
return @"Weekend";
}else {
return @"Workday";
}
}] filter:^BOOL(id _Nullable value) {
return [value isEqualToString:@"Weekend"];
}];
[blogSignalD subscribeNext:^(id _Nullable x) {
NSLog(@"Wow Weekend! Time to Relax!");
}];

总结一下,RACSignal的基本操作主要是三个:创建(手动创建+由其他通信机制生成),订阅,以及转换。

RACSubject

上面讨论的RACSignal,其实细究起其行为,是和微博不太一样的。从代码1代码3中可以看出,RACSignal所能发出的信号是定义好的,即创建该Signal的时候就已经确定了。这更像是一个「微博机器人」,每当有一个新的粉丝来订阅它,它便按照一个「创建脚本程序」从头开始生成若干微博进行推送。这种行为是依赖于「订阅」的,只有当「订阅」发生的时候,才会对新的订阅者发送内容。我们称之为「冷信号(Cold Signal)」。

这种「Cold Signal」会带来一个问题。譬如说,这个机器人在它的「创建脚本程序」中进行了其他的操作:

1
2
3
4
5
6
7
8
9
10
11
// 代码7
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSLog(@"Ready To Send!");
[subscriber sendNext:@"blog1"];
[subscriber sendNext:@"blog2"];
[subscriber sendNext:@"blog3"];
NSLog(@"Sending Comleted!");
[subscriber sendCompleted];
return nil;
}];

那么,每当有一个新的订阅者,这两个NSLog的操作就会重复执行一遍。我们把这种操作称为「副作用(Side-Effect)」。想象一下如果把上面简单的NSLog换成非常复杂的操作,比如网络请求,那么这样的「副作用」就非常明显了。因为我们可能只是想进行一次网络请求。RAC中主要使用RACSubject来解决这个问题。

RACSubjectRACSignal的子类。是不同于只会根据脚本发送固定信号的RACSignalRACSubject能够由我们程序控制,在任何时候主动发送新的值。这有点类似于不可变数组和可变数组的概念。可以想象这样一种情景,我们要在原有的旧代码里利用RAC完成一些功能,那么可以利用RACSubject,在老代码中间手动控制其发送出信号。因此,官方称RACSubject为「most helpful in bridging the non-RAC world to RAC」。

RACSubject是一个「热信号」,它在内部维护了一个「订阅者」的统计数组。每当产生新的订阅行为的时候,它只是简单地将这个「订阅者」添加进自己维护的数组中。等到发出信号的时候,会遍历该数组,向其中所有的「订阅者」发送该信号。也就是说,它不会管有没有订阅行为,而只是自己发自己的信号。而订阅之后,也只能收到它以后发出的信号。嗯,这样的行为才是一个活生生的大V博主,而不是冰冷的脚本嘛!

同时,RACSubject还是一个「订阅者」,它实现了RACSubscriber协议,也就是说,它可以订阅一个「RACSignal」。当接受到「RACSignal」发送的信号的时候,它会遍历其内部的「订阅者」数组,将自己接收到的信号转发给每一个「订阅者」。也就是说,RACSubject充当了一个中间转发者的角色。这样既保证了对原始信号只订阅一次,从而可以消除副作用的影响,又保证了外界多个订阅者的正常行为。



在RAC中,这种关系是由RACMulticastConnection类以及multicastconnect操作实现的:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 代码8
RACSignal *sideEffectSignal =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSLog(@"This is side-effect~~~");
[subscriber sendNext:@"1"];
[subscriber sendNext:@"2"];
[subscriber sendNext:@"3"];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
}];
}];
RACSubject *subject = [RACSubject subject];
RACMulticastConnection *multiConnect = [sideEffectSignal multicast:subject];
//subscribe A
[multiConnect.signal subscribeNext:^(id _Nullable x) {
NSLog(@"A receive next: %@",x);
} error:^(NSError * _Nullable error) {
NSLog(@"A receive error: %@",error);
} completed:^{
NSLog(@"A receive completed");
}];
//subscribe B
[multiConnect.signal subscribeNext:^(id _Nullable x) {
NSLog(@"B receive next: %@",x);
} error:^(NSError * _Nullable error) {
NSLog(@"B receive error: %@",error);
} completed:^{
NSLog(@"B receive completed");
}];
[multiConnect connect];
/*
Log result:
This is side-effect~~~
A receive next: 1
B receive next: 1
A receive next: 2
B receive next: 2
A receive next: 3
B receive next: 3
A receive completed
B receive completed
*/

RACMulticastConnection类封装了原始信号以及充当「中间人」的RACSubject对象,调用[multiConnect connect]会将「中间人」与原始信号连接起来(使用封装的RACSubject订阅原始RACSignal)。注意这里调用[multiConnect connect]的时机,如果将其提前到subscribe Asubscribe B之前,那么A和B将完全接收不到原始信号发出的消息,这还是因为RACSubject是一个「热信号」的原因。如果确实需要先执行connect操作,那么在创建RACMulticastConnection时可以使用RACSubject的子类,如RACReplaySubject等来实现具体的需求。

RACSequence

上面说的RACSignal是一个Push-driven的值流,而RACSequence则是一个Pull-driven的值流,它们的关系就好像是后台推送和客户端主动拉取两种不同行为。

RACSequence主要用于简化集合的操作,以及对Cocoa中的基础集合类型提供函数性的工具。譬如说,

1
2
3
4
5
6
7
// 代码9
NSArray *names = @[@"Peter",@"John",@"Steve",@"Jim"];
[[names.rac_sequence.signal filter:^BOOL(NSString * _Nullable value) {
return value.length > 4;
}] subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
}];

一般情况下,RACSequence会采用惰性计算,即要获取其中某个元素的时候再去对该元素进行计算。具体思想可以参考臧老师的聊一聊iOS开发中的惰性计算

RACCommand

除了上面讨论的几种信号,RAC还为我们提供了很多实用而充满技巧的工具类。RACCommand就是其中一个。顾名思义,它是对一个「操作命令」的封装:这个操作命令会产生一系列的结果输出,而RACCommand提供了丰富的接口来控制该操作命令的执行、取消,操作的状态流等等。

想象一下「人民日报」微博的小编,他掌握着一份程序,能够从人民日报官网拉取当天最新的新闻,将这些新闻生成一系列的微博发出。有了这个程序,他每天的工作就很轻松了:执行一下这个程序就可以了(小编不要打我…)

RACCommand提供了两个初始化方法:

1
2
- (instancetype)initWithSignalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;
- (instancetype)initWithEnabled:(nullable RACSignal<NSNumber *> *)enabledSignal signalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;

其中signalBlock就是上面说的「会产生一系列结果输出的操作命令」,而enabledSignal的值则控制了是否能执行该操作。RACCommand提供了excute接口来执行操作命令:

1
- (RACSignal<ValueType> *)execute:(nullable InputType)input;

成功执行操作后,产生的结果由executionSignals返回,每次成功执行,都会返回一个RACSignal,所以该属性是一个「高阶信号」,即「Signal of signal」;倘若执行失败,则会由errors返回:

1
2
@property (nonatomic, strong, readonly) RACSignal<RACSignal<ValueType> *> *executionSignals;
@property (nonatomic, strong, readonly) RACSignal<NSError *> *errors;

RACCommand还提供了监控当前操作状态的属性:

1
2
@property (nonatomic, strong, readonly) RACSignal<NSNumber *> *executing;
@property (nonatomic, strong, readonly) RACSignal<NSNumber *> *enabled;

举个栗子:

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
33
34
35
36
37
38
39
40
41
// 代码10
RACSignal *enalbeSignal =
[[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"Sunday"];
[subscriber sendNext:@"Monday"];
[subscriber sendNext:@"Tuesday"];
[subscriber sendNext:@"Wednesday"];
[subscriber sendNext:@"Thursday"];
[subscriber sendNext:@"Friday"];
[subscriber sendNext:@"Saturday"];
[subscriber sendCompleted];
return nil;
}] map:^id _Nullable(NSString * _Nullable value) {
if ([value isEqualToString:@"Sunday"] || [value isEqualToString:@"Saturday"]) {
return @(NO));
}else {
return @(YES));
}
}];
self.command =
[[RACCommand alloc] initWithEnabled:enalbeSignal
signalBlock:^RACSignal * _Nonnull(id _Nullable input) {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[self fetchNewsWithCallback:^(NSError *error, id result) {
if (result) {
[subscriber sendNext:result];
[subscriber sendCompleted];
}
else {
[subscriber sendError:error];
}
}];
return [RACDisposable disposableWithBlock:^{
NSLog(@"send command disposable");
}];
}] ;
}];
// The final signal for blog
RACSignal *blogSignal = [self.command.executionSignals flatten];

上面的例子创建了一个RACCommand,用来帮助小编同学在工作日从服务器拉取新闻,然后发送微博。需要注意的是,RACCommand在执行操作后,已将该操作生成的RACSignalRACReplaySubject进行了multicast,所以不用担心内部操作所包含的副作用问题。

可以看出,RACCommandUIButton的作用是很相似的:都是用于执行某个操作。事实上,RAC为UIButton提供了十分方便的category,能生成一个RACCommand并绑定到button上,使得该button的点击事件、enable状态等等都可以通过这个RACCommand完成:

1
self.senderButton.rac_command = self.command;

总结

这里我们用微博的例子来简单介绍了一下RAC中一些基础组件的用法。RAC的功能远远不止这几个基础组件,甚至远远不止是组件所提供的api。它更代表一种编程风格,一种代码思想。

当然,从这篇文章,以及自己的实践也可以看出,RAC还是存在一些缺点:

  • RAC对代码的侵入性很强,如果选择了使用它,项目代码将和RAC库产生很强的耦合。
  • RAC不利于团队协作。如果有些团队成员不熟悉,那么将很难调试和修改其他成员用RAC编写的代码。
  • 调试不友好。由于RAC内部操作相当复杂,即使一行简单的代码,调试时的堆栈也完全是RAC内部的堆栈信息。也因此,RAC官方更推荐使用namelog进行调试。
  • 学习曲线还是比较陡峭的,需要理解函数响应式编程的思想,以及学习RAC的基本知识。调bug的时候甚至还需要充分了解RAC内部原理。

因此,是否使用RAC到实际的大型项目中,这是个见仁见智的话题。但是一些小项目或是自己学习、研究、使用,还是十分有价值的。


Reference

霜神解读RAC源码系列
美团点评技术博客的RAC系列
Draveness解读的RAC源码系列
ReactiveCocoa and MVVM, an Introduction