本文简单介绍了ReactiveCocoa的基础用法,希望读完能对这个框架的使用有一个大概的了解。
前面几篇文章,我们研究了ReactiveCocoa(以下简称RAC)的起源和思想。网络上介绍RAC的使用的好文甚多,在文末的Reference中标出了一些以供参考。RAC这个开源库本身也是十分良心,代码注释十分齐全,因此这一篇文章不做太多的扩展,仅谈一谈RAC的基础使用方法。
RACSignal
上文中说到,函数响应式编程中,将各种通信机制所需要解决的「输入」与「输出」的异步关系抽象成了事件/时间驱动的值流,并通过monad
使其支持了函数式编程的特性。而在RAC中,这个东西就是RACStream
,开发过程中我们并不是直接使用它,而是其子类——RACSignal
和RACSequence
。这一节,讲讲RACSignal
。
Signal,顾名思义,代表一个信号,可以源源不断地给你传递信息。这样就好理解RACSignal
代表着「随时间变化的值流」,这里的值,就包含了将来即将到来的「输入」。打个比方,一个微博博主便是一个「Signal」,只要没被封号,你就会知道将来他会一直发出消息。如果关注了这个博主,一旦他开始发消息,新消息会被自动推送到你的设备,因此说RACSignal
是一个Push-Driven
的值流。
那么,RACSignal
博主会发出什么消息呢?一个RACSignal
传递的值分为三类:
- Next。「Next」代表着一个新的值,一条新的微博。只要这个博主是活跃的,他就会源源不断地发微博。
- Error。「Error」则代表着这个Signal出了什么问题,发出了一个代表「错误」的信号。发送出「Error」也就意味着这个Signal的消息到此为止了。比如这位博主被封号了,他就会给你发一条微博,上面写着「404Error」,你就知道他再也不会发微博了……
- Completed。代表一个Signal完成了自己的全部信息发送。比如某天这个博主想退出微博了,于是发出最后一条微博——「ByeBye粉丝们」。这就是「Completed」。
一个Signal的信息流,都是由若干个「Next」,加上一个代表终结的「Error」或「Completed」组成的。
这些值都是从哪里来的呢?一个Signal所发出的信息主要来源有两种:
- 手动创建一个信号时定义它发出的信息。这就好像是一位原创博主,每条微博都是他自己写的:
// 代码1
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) {
[subscriber sendNext:@"blog1"];
[subscriber sendNext:@"blog2"];
[subscriber sendNext:@"blog3"];
[subscriber sendCompleted];
return nil;
}];
- 由其他通信机制生成一个信号时,在其他通信机制产生输入时发出消息。比如某些大V博主的微博就是专门从杂志、知乎等其他信息载体上将信息搬运过来。RAC提供了很多有力的工具,让我们从传统的Cocoa通信机制中制造出一个信号来:
// 代码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)。例如,我们想在收到消息时,把消息打印出来,或者做一些其他的事情:
// 代码3
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id _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
这位博主,他发出的blog1
,blog2
等等微博都会推送到我们,由我们进行处理。RAC对于信号的「订阅者」是有要求的,它必须实现了RACSubscriber
协议:
// 代码4
@protocol RACSubscriber
@required
- (void)sendNext:(id)value;
- (void)sendError:(NSError *)error;
- (void)sendCompleted;
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
@end
这也很好理解,因为「订阅者」至少得知道自己需要用这些订阅的值来做什么。上面的代码3
中的subscribeNext:error:completed:
其实就是帮我们创建了一个内部的「订阅者」,这些在后续如果深入探究源码的时候会详细说明。
此外,Signal支持各种函数式的操作,例如map
,reduce
,filter
等等。这可以让我们方便地对原始信号传输出的信息进行一步步加工,最终得到我们所需要的值,这就是「函数性」赋予的利器:
// 代码5
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id _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
的保证,我们也可以采用链式语法这么写:
// 代码6
RACSignal *blogSignalD =
[[[RACSignal createSignal:^RACDisposable * _Nullable(id _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」会带来一个问题。譬如说,这个机器人在它的「创建脚本程序」中进行了其他的操作:
// 代码7
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id _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
来解决这个问题。
RACSubject
是RACSignal
的子类。是不同于只会根据脚本发送固定信号的RACSignal
,RACSubject
能够由我们程序控制,在任何时候主动发送新的值。这有点类似于不可变数组和可变数组的概念。可以想象这样一种情景,我们要在原有的旧代码里利用RAC完成一些功能,那么可以利用RACSubject
,在老代码中间手动控制其发送出信号。因此,官方称RACSubject
为「most helpful in bridging the non-RAC world to RAC」。
RACSubject
是一个「热信号」,它在内部维护了一个「订阅者」的统计数组。每当产生新的订阅行为的时候,它只是简单地将这个「订阅者」添加进自己维护的数组中。等到发出信号的时候,会遍历该数组,向其中所有的「订阅者」发送该信号。也就是说,它不会管有没有订阅行为,而只是自己发自己的信号。而订阅之后,也只能收到它以后发出的信号。嗯,这样的行为才是一个活生生的大V博主,而不是冰冷的脚本嘛!
同时,RACSubject
还是一个「订阅者」,它实现了RACSubscriber
协议,也就是说,它可以订阅一个「RACSignal」。当接受到「RACSignal」发送的信号的时候,它会遍历其内部的「订阅者」数组,将自己接收到的信号转发给每一个「订阅者」。也就是说,RACSubject
充当了一个中间转发者的角色。这样既保证了对原始信号只订阅一次,从而可以消除副作用的影响,又保证了外界多个订阅者的正常行为。
在RAC中,这种关系是由RACMulticastConnection
类以及multicast
和connect
操作实现的:
// 代码8
RACSignal *sideEffectSignal =
[RACSignal createSignal:^RACDisposable * _Nullable(id _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 A
和subscribe B
之前,那么A和B将完全接收不到原始信号发出的消息,这还是因为RACSubject
是一个「热信号」的原因。如果确实需要先执行connect
操作,那么在创建RACMulticastConnection
时可以使用RACSubject
的子类,如RACReplaySubject
等来实现具体的需求。
RACSequence
上面说的RACSignal
是一个Push-driven
的值流,而RACSequence
则是一个Pull-driven
的值流,它们的关系就好像是后台推送和客户端主动拉取两种不同行为。
RACSequence
主要用于简化集合的操作,以及对Cocoa中的基础集合类型提供函数性的工具。譬如说,
// 代码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
提供了两个初始化方法:
- (instancetype)initWithSignalBlock:(RACSignal * (^)(InputType _Nullable input))signalBlock;
- (instancetype)initWithEnabled:(nullable RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(InputType _Nullable input))signalBlock;
其中signalBlock
就是上面说的「会产生一系列结果输出的操作命令」,而enabledSignal
的值则控制了是否能执行该操作。RACCommand
提供了excute
接口来执行操作命令:
- (RACSignal *)execute:(nullable InputType)input;
成功执行操作后,产生的结果由executionSignals
返回,每次成功执行,都会返回一个RACSignal
,所以该属性是一个「高阶信号」,即「Signal of signal」;倘若执行失败,则会由errors
返回:
@property (nonatomic, strong, readonly) RACSignal *> *executionSignals;
@property (nonatomic, strong, readonly) RACSignal *errors;
RACCommand
还提供了监控当前操作状态的属性:
@property (nonatomic, strong, readonly) RACSignal *executing;
@property (nonatomic, strong, readonly) RACSignal *enabled;
举个栗子:
// 代码10
RACSignal *enalbeSignal =
[[RACSignal createSignal:^RACDisposable * _Nullable(id _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 _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
在执行操作后,已将该操作生成的RACSignal
用RACReplaySubject
进行了multicast
,所以不用担心内部操作所包含的副作用问题。
可以看出,RACCommand
和UIButton
的作用是很相似的:都是用于执行某个操作。事实上,RAC为UIButton
提供了十分方便的category,能生成一个RACCommand
并绑定到button上,使得该button的点击事件、enable状态等等都可以通过这个RACCommand
完成:
self.senderButton.rac_command = self.command;
总结
这里我们用微博的例子来简单介绍了一下RAC中一些基础组件的用法。RAC的功能远远不止这几个基础组件,甚至远远不止是组件所提供的api。它更代表一种编程风格,一种代码思想。
当然,从这篇文章,以及自己的实践也可以看出,RAC还是存在一些缺点:
- RAC对代码的侵入性很强,如果选择了使用它,项目代码将和RAC库产生很强的耦合。
- RAC不利于团队协作。如果有些团队成员不熟悉,那么将很难调试和修改其他成员用RAC编写的代码。
- 调试不友好。由于RAC内部操作相当复杂,即使一行简单的代码,调试时的堆栈也完全是RAC内部的堆栈信息。也因此,RAC官方更推荐使用
name
或log
进行调试。 - 学习曲线还是比较陡峭的,需要理解函数响应式编程的思想,以及学习RAC的基本知识。调bug的时候甚至还需要充分了解RAC内部原理。
因此,是否使用RAC到实际的大型项目中,这是个见仁见智的话题。但是一些小项目或是自己学习、研究、使用,还是十分有价值的。
Reference
霜神解读RAC源码系列
美团点评技术博客的RAC系列
Draveness解读的RAC源码系列
ReactiveCocoa and MVVM, an Introduction