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
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    [subscriber sendNext:@"blog1"];
    [subscriber sendNext:@"blog2"];
    [subscriber sendNext:@"blog3"];
    [subscriber sendCompleted];
    return nil;
}];
  1. 由其他通信机制生成一个信号时,在其他通信机制产生输入时发出消息。比如某些大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<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协议:

// 代码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等等。这可以让我们方便地对原始信号传输出的信息进行一步步加工,最终得到我们所需要的值,这就是「函数性」赋予的利器:

// 代码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的保证,我们也可以采用链式语法这么写:

// 代码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」会带来一个问题。譬如说,这个机器人在它的「创建脚本程序」中进行了其他的操作:

// 代码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充当了一个中间转发者的角色。这样既保证了对原始信号只订阅一次,从而可以消除副作用的影响,又保证了外界多个订阅者的正常行为。

<div align=center>


image

</div>

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

// 代码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中的基础集合类型提供函数性的工具。譬如说,

// 代码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<ValueType> * (^)(InputType _Nullable input))signalBlock;
- (instancetype)initWithEnabled:(nullable RACSignal<NSNumber *> *)enabledSignal signalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;

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

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

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

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

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

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

举个栗子:

// 代码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完成:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容