ReactiveCocoa2 源码浅析

  • 开车不需要知道离合器是怎么工作的,但如果知道离合器原理,那么车子可以开得更平稳。

ReactiveCocoa 是一个重型的 FRP 框架,内容十分丰富,它使用了大量内建的 block,这使得其有强大的功能的同时,内部源码也比较复杂。本文研究的版本是2.4.4,小版本间的差别不是太大,无需担心此问题。 这里只探究其核心 RACSignal 源码及其相关部分。本文不会详细解释里面的代码,重点在于讨论那些核心代码是怎么来的。文本难免有不正确的地方,请不吝指教,非常感谢。

@protocol RACSubscriber

信号是一个异步数据流,即一个将要发生的以时间为序的事件序列,它能发射出三种不同的东西:value、error、completed。咱们能异步地捕获这些事件:监听信号,针对其发出的三种东西进行操作。“监听”信息的行为叫做 订阅(subscriber)。我们定义的操作就是观察者,这个被“监听”的信号就是被观察的主体(subject) 。其实,这正是“观察者”设计模式!

RAC 针对这个订阅行为定义了一个协议:RACSubscriber。RACSubscriber 协议是与 RACSignal 打交道的唯一方式。咱们先不探究 RACSignal 的内容,而是先研究下 RACSubscriber 是怎么回事。

先来看下 RACSubscriber 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 用于从 RACSignal 中直接接收 values 的对象
@protocol RACSubscriber
@required
 
/// 发送下一个 value 给 subscribers。value 可以为 nil。
- (void)sendNext:(id)value;
 
/// 发送 error 给 subscribers。 error 可以为 nil。
///
/// 这会终结整个订阅行为,而且接下来也无法再订阅任何信号了。
- (void)sendError:(NSError *)error;
 
/// 发送 completed 给 subscribers。
///
/// 这会终结整个订阅行为,而且接下来也无法再订阅任何信号了。
- (void)sendCompleted;
 
/// 现在重要的是上面三个,先别管这个,忽略掉。
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
 
@end

1、NLSubscriber

咱们自己来实现这个协议看看(本文自定义的类都以 “NL” 开头,以视区别):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// NLSubscriber.h
@interface NLSubscriber : NSObject @end
// NLSubscriber.m
@implementation NLSubscriber
- (void)sendNext:(id)value {
   NSLog(@ "%s value:%@" , sel_getName(_cmd), value);
}
- (void)sendCompleted {
   NSLog(@ "%s" , sel_getName(_cmd));
}
- (void)sendError:(NSError *)error {
   NSLog(@ "%s error:%@" , sel_getName(_cmd), error);
}
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable {
   // to nothing
}
@end

现在咱们这个类只关心 sendNext: 、 sendError: 和 sendCompleted。本类的实现只是简单的打印一些数据。那怎么来使用这个订阅者呢?RACSignal 类提供了接口来让实现了 RACSubscriber 协议的订阅者订阅信号:

1
2
3
4
5
6
7
@interface RACSignal (Subscription)
/*
*  `subscriber` 订阅 receiver 的变化。由 receiver 决定怎么给 subscriber 发送事件。
*简单来说,就是由这个被订阅的信号来给订阅者 subscriber 发送 `sendNext:` 等消息。
*/
- (RACDisposable *)subscribe:(id)subscriber;
@end

用定时器信号来试试看:

1
2
3
4
5
6
7
8
9
10
11
/**
*  @brief  创建一个定时器信号,每三秒发出一个当时日期值。一共发5次。
*/
RACSignal *signalInterval = [RACSignal interval:3.0 onScheduler:[RACScheduler mainThreadScheduler]];
signalInterval = [signalInterval take:5];
NLSubscriber *subscriber = [[NLSubscriber alloc] init];
 
/**
*  @brief  用订阅者 subscriber 订阅定时器信号
*/
[signalInterval subscribe:subscriber];

下面是输出结果:

1
2
3
4
5
6
2015-08-15 17:45:02.612 RACPraiseDemo[738:59818] sendNext: value:2015-08-15 09:45:02 +0000
2015-08-15 17:45:05.612 RACPraiseDemo[738:59818] sendNext: value:2015-08-15 09:45:05 +0000
2015-08-15 17:45:08.615 RACPraiseDemo[738:59818] sendNext: value:2015-08-15 09:45:08 +0000
2015-08-15 17:45:11.613 RACPraiseDemo[738:59818] sendNext: value:2015-08-15 09:45:11 +0000
2015-08-15 17:45:14.615 RACPraiseDemo[738:59818] sendNext: value:2015-08-15 09:45:14 +0000
2015-08-15 17:45:14.615 RACPraiseDemo[738:59818] sendCompleted

2、改进NLSubscriber

现在的这个订阅者类 NLSubscriber 除了打印打东西外,啥也干不了,更别说复用了,如果针对所有的信号都写一个订阅者那也太痛苦了,甚至是不太可能的事。

咱们来改进一下,做到如下几点:

a.实现 RACSubscriber 协议

b.提供与 RACSubscriber 相 对应的 、可选的 、可配的接口。

没错,这正是一个适配器!

第2点的要求可不少,那怎么才能做到这一点呢?还好,OC 中有 block !咱们可以将 RACSubscriber 协议中的三个方法转为三个 block:

1
2
3
- (void)sendNext:(id)value;           ---->    void (^next)(id value);
- (void)sendError:(NSError *)error;   ---->    void (^error)(NSError *error);
- (void)sendCompleted;                ---->    void (^completed)(void);

改进目标和改进方向都有了,那咱们来看看改进后的的样子:

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
49
50
// 头文件
/**
  *  @brief  基于 block 的订阅者
  */
@interface NLSubscriber : NSObject  /**
  *  @brief  创建实例
  */
+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed;
@end
// 实现文件
@interface NLSubscriber ()
@property (nonatomic, copy) void (^next)(id value);
@property (nonatomic, copy) void (^error)(NSError *error);
@property (nonatomic, copy) void (^completed)(void);
@end
@implementation NLSubscriber
#pragma mark Lifecycle
+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed {
   NLSubscriber *subscriber = [[self alloc] init];
   subscriber->_next = [next copy];
   subscriber->_error = [error copy];
   subscriber->_completed = [completed copy];
   return  subscriber;
}
#pragma mark RACSubscriber
- (void)sendNext:(id)value {
   @synchronized (self) {
     void (^nextBlock)(id) = [self.next copy];
     if  (nextBlock == nil)  return ;
     nextBlock(value);
   }
}
- (void)sendError:(NSError *)e {
   @synchronized (self) {
     void (^errorBlock)(NSError *) = [self.error copy];
     if  (errorBlock == nil)  return ;
     errorBlock(e);
   }
}
- (void)sendCompleted {
   @synchronized (self) {
     void (^completedBlock)(void) = [self.completed copy];
     if  (completedBlock == nil)  return ;
     completedBlock();
   }
}
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable {
   // to nothing
}
@end

现在来试试看这个改进版,还是上面那个定时器的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
    *  @brief  创建一个定时器信号,每三秒发出一个当时日期值。一共发5次。
    */
   RACSignal *signalInterval = [RACSignal interval:3.0 onScheduler:[RACScheduler mainThreadScheduler]];
   signalInterval = [signalInterval take:5];
   NLSubscriber *subscriber = [NLSubscriber subscriberWithNext:^(id x) {
     NSLog(@ "next:%@" , x);
   } error:nil completed:^{
     NSLog(@ "completed" );
   }];
   /**
    *  @brief  用订阅者 subscriber 订阅定时器信号
    */
   [signalInterval subscribe:subscriber];

输出结果如下:

1
2
3
4
5
6
2015-08-15 19:50:43.355 RACPraiseDemo[870:116551] next:2015-08-15 11:50:43 +0000
2015-08-15 19:50:46.358 RACPraiseDemo[870:116551] next:2015-08-15 11:50:46 +0000
2015-08-15 19:50:49.355 RACPraiseDemo[870:116551] next:2015-08-15 11:50:49 +0000
2015-08-15 19:50:52.356 RACPraiseDemo[870:116551] next:2015-08-15 11:50:52 +0000
2015-08-15 19:50:55.356 RACPraiseDemo[870:116551] next:2015-08-15 11:50:55 +0000
2015-08-15 19:50:55.356 RACPraiseDemo[870:116551] completed

输出结果没什么变化,但是订阅者的行为终于受到咱们的撑控了。再也不用为了一个信号而去实现 RACSubscriber 协议了,只需要拿出 NLSubscriber 这个适配器,再加上咱们想要的自定义的行为即可。如果对信号发出的某个事件不感兴趣,直接传个 nil 可以了,例如上面例子的 error: ,要知道, RACSubscriber 协议中的所有方法都是 @required 的。NLSubscriber 大大方便了我们的工作。

那还以再改进吗?

3、RACSignal 类别之 Subscription

有没有可能把 NLSubscriber 隐藏起来呢?毕竟作为一个信号的消费者,需要了解的越少就越简单,用起来也就越方便。咱们可以通过 OC 中的类别方式,给 RACSignal 加个类别(nl_Subscription),将订阅操作封装到这个信号类中。这样,对于使用这个类的客户而言,甚至不知道订阅者的存在。

nl_Subscription 类别代码如下:

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
// .h 
#import "RACSignal.h"
@interface RACSignal (nl_Subscription)
- (void)nl_subscribeNext:(void (^)(id x))nextBlock;
- (void)nl_subscribeNext:(void (^)(id x))nextBlock completed:(void (^)(void))completedBlock;
- (void)nl_subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock;
- (void)nl_subscribeError:(void (^)(NSError *error))errorBlock;
- (void)nl_subscribeCompleted:(void (^)(void))completedBlock;
- (void)nl_subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock;
- (void)nl_subscribeError:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock;
@end
// .m
#import "RACSignal+nl_Subscription.h"
#import "NLSubscriber.h"
@implementation RACSignal (nl_Subscription)
- (void)nl_subscribeNext:(void (^)(id x))nextBlock {
   [self nl_subscribeNext:nextBlock error:nil completed:nil];
}
- (void)nl_subscribeNext:(void (^)(id x))nextBlock completed:(void (^)(void))completedBlock {
   [self nl_subscribeNext:nextBlock error:nil completed:completedBlock];
}
- (void)nl_subscribeError:(void (^)(NSError *error))errorBlock {
   [self nl_subscribeNext:nil error:errorBlock completed:nil];
}
- (void)nl_subscribeCompleted:(void (^)(void))completedBlock {
   [self nl_subscribeNext:nil error:nil completed:completedBlock];
}
- (void)nl_subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock {
   [self nl_subscribeNext:nextBlock error:errorBlock completed:nil];
}
- (void)nl_subscribeError:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock {
   [self nl_subscribeNext:nil error:errorBlock completed:completedBlock];
}
- (void)nl_subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock {
   NLSubscriber *subscriber = [NLSubscriber subscriberWithNext:nextBlock error:errorBlock completed:completedBlock];
   [self subscribe:subscriber];
}
@end

在这个类别中,将信号的 next:、error: 和 completed 以及这三个事件的组合都以 block 的形式封装起来,从以上代码中可以看出,这些方法最终调用的还是 - (void)nl_subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock; 方法,而它则封装了订阅者 NLSubsciber。

通过这么个小小的封装,客户使用起来就极其方便了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
    *  @brief  创建一个自定义的信号。
    *        这个信号在被订阅时,会发送一个当前的日期值;
    *        再过三秒后,再次发送此时的日期值;
    *        最后,再发送完成事件。
    */
   RACSignal *signalInterval = [RACSignal createSignal:^(id subscriber) {
     [subscriber sendNext:[NSDate date]];
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
       [subscriber sendNext:[NSDate date]];
       [subscriber sendCompleted];
     });
     return  (id)nil;
   }];
   [signalInterval nl_subscribeNext:^(id x) {
     NSLog(@ "next:%@" , x);
   } error:^(NSError *error) {
     NSLog(@ "error:%@" , error);
   } completed:^{
     NSLog(@ "completed" );
   }];

输出如下:

1
2
3
2015-08-16 23:29:44.406 RACPraiseDemo[653:32675] next:2015-08-16 15:29:44 +0000
2015-08-16 23:29:47.701 RACPraiseDemo[653:32675] next:2015-08-16 15:29:47 +0000
2015-08-16 23:29:47.701 RACPraiseDemo[653:32675] completed

本例并没有采用之前的 “定时器信号”,而是自己创建的信号,当有订阅者到来时,由这个信号来决定在什么时候发送什么事件。这个例子里发送的事件的逻辑请看代码里的注释。

看到这里,是不是很熟悉了?有没有想起 subscribeNext:,好吧,我就是在使用好多好多次它之后才慢慢入门的,谁让 RAC 的大部分教程里面第一个讲的就是它呢!

到了这里,是不是订阅者这部分就完了呢?我相信你也注意到了,这里有几个不对劲的地方:

a.无法随时中断订阅操作。想想订阅了一个无限次的定时器信号,无法中断订阅操作的话,定时器就是永不停止的发下去。

b.订阅完成或错误时,没有统一的地方做清理、扫尾等工作。比如现在有一个上传文件的信号,当上传完成或上传错误时,你得断开与文件服务器的网络连接,还得清空内存里的文件数据。


再探FlattenMap与Map

问题提出

有时候,我们需要把一个异步的API用信号的方式来表示。比如,点击登录按钮后异步的访问服务器,当获取到数据的时候再调用订阅者的处理方法。一个可能会出现的代码大概是这样:

- (RACSignal *)signInSignal {
return [RACSignal createSignal:^RACDisposable *(id subscriber){
[self.signInService
signInWithUsername:self.usernameTextField.text
password:self.passwordTextField.text
complete:^(BOOL success){
[subscriber sendNext:@(success)];
[subscriber sendCompleted];
}];
return nil;
}];
}
[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
map:^id(id x){
return [self signInSignal];
}]
subscribeNext:^(id x){
NSLog(@"Sign in result: %@", x);
}];

这样的代码并不能正常运行,原文给出的解决方案是把map方法换成flattenMap方法,但是却没有给出解释。在此专栏的上一篇文章,我们已经研究过了flattenMap的工作原理,并且明确了一点:map方法是基于flattenMap方法实现的。


再探FlattenMap与Map

在了解这两个方法的工作原理之前,我们不妨梳理一下思路。对于一个信号流(由若干个信号前后拼接而成)中的每一个信号而言,我们最关心它能传递出什么数据,就像我们只关心水龙头里面流出的是水还是石油一样。至于流入的数据,一定是上一个信号的流出数据。

然而,在之前的讨论中我们已经清楚,信号能流出什么样的数据,是在创建这个信号的决定的。也就是说,订阅者拿到的数据,取决于在创建信号的时候,我们制定的sendNext方法的参数。与现实生活不同的是,我们可以在水龙头之间添加过滤网来实现类似的功能。

在一文中,我们已经详细的查看了bind方法和flattenMap方法的代码和文档。

flattenMap方法,实际上是根据前一个信号传递进来的参数重新建立了一个信号,这个参数,可能会在创建信号的时候用到,也有可能根本用不到。比如在之前的例子中,我们其实调用了自定义的方法来创建信号。

之前提到过,我们关注一个信号能传递什么数据出来,那么调用了flattenMap方法创建的信号,会传出什么样的值呢?

答案是不知道!!!

因为flattenMap方法并不关心生成的信号会传递什么值,它只负责

根据前一个信号的参数创建一个新的信号!

而至于这个信号会传递什么值,之前也提到过,是在创建信号的时候指定的。比如本文所举的例子中,我们自定义的信号会传递这样的值

[subscriber sendNext:@(success)]

理解了这一点之后,map方法就简单了。先看一下map方法的定义:

- (instancetype)map:(id (^)(id value))block {
NSCParameterAssert(block != nil);
Class class = self.class;
return [[self flattenMap:^(id value) {
return [class return:block(value)];
}] setNameWithFormat:@"[%@] -map:", self.name];
}

这里的[class return:block(value)]方法实现了flattenMap方法的功能,他返回了一个信号。return:方法的代码实现有点长,有兴趣的读者可以自行查阅,这里就不具体分析,总结来说就是,这个信号传递的值讲是block(value)。

flattenMap方法和map方法都有一个带参数value的block作为这个方法的参数。不同的是,flattenMap方法通过调用block(value)来创建一个新的方法。它可以灵活的定义新创建的信号。而map方法,将会创建一个和原来一模一样的信号,只不过新的信号传递的值变为了block(value)。

总结一下,个人对map的理解是“变换”。map方法,根据原信号创建了一个新的信号,并且变换了信号的输出值。这两个信号具有明显的先后顺序关系。而flattenMap方法,直接生成了一个新的信号,这两个信号并没有先后顺序关系,属于同层次的平行关系。这也许就是为什么会被命名为flattenMap吧。


实践检验

回头看一看此前的例子之所以用map方法不行的原因在于,map方法创建的信号,接收了前一个信号传递的值,传出的值是[self signInSignal]的执行结果,即任然是一个信号,但是我们并不需要这个信号,我们需要的是这个信号的传出值。

使用flattenMap方法就可行的原因在于,flattenMap方法生成了一个新的信号,也就是我们调用[self signInSignal]的执行结果。这个信号的传出的值,在信号的创建过程中已经被定义。所以可以正常工作。


吐槽一句

非常感谢最初翻译出ReactiveCocoa教程的大牛,给了我们快速入门的机会。但是能力越大,责任也越大,简单一句flattenMap可以处理信号中的信号,是非常不负责任的,可能会影响无数的学习者。甚至原作者当时也没能完全理解flattenMap的工作原理以及和map方法之间的关系。我想,严谨、求真知,是对任何一门科学的基本尊重。包括软件工程。

这句话,也送给未来的自己!



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值