ReactiveCocoa2 源码浅析

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

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

@protocol RACSubscriber

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

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

先来看下 RACSubscriber 的定义:

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

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

用定时器信号来试试看:

下面是输出结果:

##2、改进NLSubscriber

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

咱们来改进一下,做到如下几点:
1. 实现 RACSubscriber 协议
2. 提供与 RACSubscriber对应的可选的可配的接口。

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

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

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

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

输出结果如下:

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

那还以再改进吗?

##3、RACSignal 类别之 Subscription

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

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

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

输出如下:

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

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

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

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

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

##4、Disposable
###RACDisposable
针对上述两个问题,RACDisposable 应运而生。也就是说 Disposable 有两个作用:

1. 中断订阅某信号
2. 订阅完成后,执行一些收尾任务(清理、回收等等)。

订阅者与 Disposable 的关系:

1. 当 Disposable 有“清理”过,那么订阅者就不会再接收到这个被“清理”订阅源的任何事件。举例而言,就是订阅者 subscriberX 订阅了信号 signalA 和 signalB 两个信号,其所对应的 Disposable 分别为 disposableA 和 disposableB,也就是说 subscriberX 会同时接收来自 signalA 和 signalB 的信号。当我们手动强制 “清理” disposableA 后,subscriberX 就不会再接收来自 signalA 的任何事件;而来自 signalB 的事件则不受影响。

2. 当订阅者 subscriberX 有接收来自任何一个信号的 “error” 或 “completed” 事件时,则不会再接收任何事件了。

可以这么说:Disposable 代表发生了订阅行为

根据 Disposable 的作用和与订阅者的关系,来总结它所需要提供的接口:

1. 包含清理任务的 block ;
2. 执行清理任务的方法:- (void)dispose ;
3. 一个用来表明是否已经 “清理” 过的布尔变量:BOOL disposed 。

咱们为这个 Disposable 也整了一个类,如下:

从这个类提供的接口来看,显然是做不到 “订阅者与 Disposable 的关系” 中的第2条的。因为这条中所描述的是一个订阅者订阅多个信号,且能手动中断订阅其中一个信号的功能,而 NLDisposable 是单个订阅关系所设计的。

###RACCompoundDisposable
那怎么组织这“多个”的关系呢?数组?Good,就是数组。OK,咱们来相像一下这个方案的初步代码。每个订阅者有一个 Disposable 数组,订阅一个一个信号,则加入一个 Disposable;当手动拆除一个订阅关系时,找到与之相关的 Disposable,发送 dispose 消息,将其从数组中移除;当订阅者不能再接收消息时(接收过 errorcompleted 消息),要 dispose 数组中所有元素,接下来再加入元素时,直接给这个要加入的元素发送 dispose 消息;在多线程环境下,每一次加入或移除或其遍历时,都得加锁。。。(好吧,我编不下去了)

我** ,这么复杂,看来直接用数组来维护是不可行的了。有啥其它可行的法子没?还好,GoF 对此有个方案,叫做“组合模式”:

组合模式 允许你将对象组合成树形结构来表现 “整体/部分” 层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。

使用组合结构,我们能把相同的操作应用在组合和个别对象上。换句话说,在大多数情况下,我们可以 忽略 对象组合和个别对象之间的差别。

本文毕竟不是来讲模式的,关于这个模式更多的信息,请自行 google。

RAC 中这个组合类叫 RACCompoundDisposable, 咱们的叫 NLCompoundDisposable,来看看咱们这个类的代码:

 

###RACScheduler 简介

本文不打算研究 RACScheduler 源码,但其又是 RAC 中不可或缺的一个组件,在研究 RACSignal 的源码时不可避免地会遇到它,所以对其作下介绍还是有必要的。其实它的源码并不复杂,可自行研究。

ReactiveCocoa 中 RACSignal 发送的所有事件的传递交给了一个特殊的框架组件——调度器,即 RACScheduler 类簇(类簇模式稍后介绍)。调度器是为了简化 同步/异步/延迟 事件传递 以及 取消预定的任务(scheduded actions) 这两种 RAC 中常见的动作而提出来的。“事件传递” 简单而言就是些 blocks,RACScheduler 所做的就是:调度这些 blocks (schedule blokcs,还是英文的意思准确些)。我们可以通过那些调度方法所返回的 RACDisposable 对象来取消那些 scheduling blocks。

正如前面所说,RACScheduler 是一个类簇。咱们来看看几种具体的调度器:

####RACImmediateScheduler
这是 RAC 内部使用的私有调度器,只支持同步 scheduling。就是简单的马上执行 block。这个调试器的延迟 scheduling 是通过调用 -[NSThread sleepUntilDate:] 来阻塞当前线程来达到目的的。显然,这样一个调度器,没法取消 scheduling,所以它那些方法返回的 disposables 啥也不会做(实际上,它那些 scheduling 方法返回的是nil)。

####RACQueueScheduler
这个调度器使用 GCD 队列来 scheduling blocks。如果你对 GCD 有所了解的话,你会发现这个调度器的功能很简单,它只是在 GCD 队列 dispatching blocks 上的简单封装罢了。

####RACSubscriptionScheduler
这是另一个内部使用的私有调度器。如果当前线程有调度器(调度器可以与线程相关联起来:associated)那它就将 scheduling 转发给这个线程的调度器;否则就转发给默认的 background queue 调试器。

####接口
调试器有下面一些方法:

scheduling block 如下:

##5、Subscriber 和 Disposable

前面介绍了 Disposable 的来源,现在来研究下怎么使用它。还记得吗,订阅者与信号打交道的唯一方式是 RACSignal 中的一个方法:

自定义信号所对应的类是 RACDynamicSignalRACSignal 采用的是类簇模式。除自定义信号之外还有几种其它的信号,之后会研究到。OC 中的 NSNumber 用的就是类簇模式。类簇是Foundation框架中广泛使用的设计模式。类簇将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,以这种方法来组织类可以简化一个面向对象框架的公开架构,而又不减少功能的丰富性。

咱们来研究一下自定义信号里的这个方法的实现。这个方法实现的难处在于:“一个订阅者可以订阅多个信号,并可以手动拆除其中任何一个订阅”。针对这个问题,提出了上节讲到的 RACDisposable。也就是说,在每一次订阅时,都会返回一个与这次订阅相关的 Disposable,那怎么做到这一点呢?

给订阅者添加一个 CompoundDisposable 类型的属性 (毕竟 CompoundDisposable 就是用来针对多个 Disposable 的统一管理而存在的),然后在每一次订阅时,都加一个 Disposable 到这个属性里,行不行?但很可惜,订阅者是一个协议 protocol RACSubscriber,而不是一个具体的类,咱们在使用到它时,都是别人实现了这个协议的类的对象,所以咱们不太可能做到说给这么一个未知的类添加一个属性。

事实上,RAC 中确实有 RACSubscriber 这么一个私有类(它是咱们第一个自定义类 NLSubscriber 的原型),咱们叫它做 class RACSubscriber。嗯,class RACSusbscriber 实现了 protocol RACSubscriber 协议:@interface RACSubscriber : NSObject <RACSubscriber>。有没有想到 class NSObjectprotocol NSObject ?虽然它们形式上确实很像,但千万别混为一谈。RAC 中的其它实现了 protocol RACSubscriber 协议的订阅者类可没有一个继承自 class RACSubscriber 的。

咱们可以用装饰模式来解决这个问题

装饰模式。在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。

订阅者装饰器 RACPassthroughSubscriber

在订阅者每一次订阅信号时产生一个 Disposable,并将其与此次订阅关联起来,这是通过装饰器 RACPassthroughSubscriber 来做到的。这个装饰器的功能:

1. 包装真正的订阅者,使自己成为订阅者的替代者。

2. 将真正的订阅者与一个订阅时产生的 Disposable 关联起来。

这正是一个装饰器所应该做的。依之前的,咱们来模仿这个装饰器,新建一个咱们的装饰器:NLPassthroughSubscriber,来看下它的代码:

自定义信号 RACDynamicSignal 的订阅方法 subscribe

咱们来看看 RACDynamicSignal 是怎么来使用 RACPassthroughSubscriber 的,这里就不自己写代码了,直接上它的代码:

可以看到,订阅者装饰器直接伪装成真正的订阅器,传给 didSubscribe 这个 block 使用。在这个 block 中,会有一些事件发送给订阅者装饰器,而这个订阅者装饰器则根据 disposable 的状态来来决定是否转发给真正的订阅者。disposable 作为返回值,返回给外部,也就是说能够从外部来取消这个订阅了。

从这几行代码中,我们可以看到,didSubscribe 这个 block 是处于 subscriptionScheduler 这个 scheduler 的调度中。RACSubscriptionScheduler 的调度是取决于当前所在的线程的,即 didSubscribe 可能会在不同的调度器中被执行。

假设当前 -(RACDisposable *)subscribe:(id<RACSubscriber>)subscriber 这个方法是在异步环境下调用的,那么在 disposable 返回后,在schedule block 还没有来得及调用,此时 disposable 中包含 schedulingDisposable。如果我们此时给 disposable 发送 dispose 消息,那么 schedulingDisposable 也会被 dispose,schedule block 就不会执行了;如果是在 schedule block 执行中或执行后给 disposable 发送 dispose 消息,那么 innerDisposableschedulingDisposable 都会被 dispose。这些行为正是咱们所预期的。

##6、再次改进NLSubscriber
###1、didSubscribeWithDisposable

这个 RACSubscriber 协议中声明的一个方法,在最开始的时候被我们特意给忽略,现在是时候回过头来看看它了。对于一个订阅者来说,nexterrorcompleted 三种事件分别对应协议里的三种方法,那么这个方法存在的意义是什么呢?

RACSubscriber 协议中,可以看到,当一个订阅者有收到过 errorcompleted 事件后,这个订阅者就不能再接收任何事件了,换句话说,此时这个订阅者会解除所有的订阅关系,且无法再次订阅。既然要解除所有订阅,首先我得知道我订阅过哪些信号是不?而代表一个订阅行为的就是 disposable ,告诉它就传一个给它好了。所以这个方法就是告诉订阅者:你发生了订阅行为。

那为啥要 RACCompoundDisposable 类型作为参数呢?因为有些订阅者会针对其附加一些操作,而只有这个类型的 disposable 才能动态加入一些操作。接下来我们就会看到的。

###2、NLSubscriber 结合 RACDisposable
这一次改进 NLSubscriber 的目的是让其可以终结自己的订阅能力的功能。同时实现 didSubscribeWithDisposable 方法。千言万语不如实际代码,让我们来一探究竟:

###3、改进类别 nl_Subscription
还记得么?nl_Subscription 类别中的订阅方法一旦订阅,就无法停止了,这显然有很大的问题。解决这个问题很简单,直接将 disposable 返回即可:

 

RACSignal – Operations

本节主要研究这些操作(Operations) —— flattenMap:map:filter: ….

终于看到你想看的东西了?好吧,我承认,上节的东西很无趣,可能压根不是你想看的东西。但如果没弄清上面的内容的话,直接研究 Operations 可是会比较吃力的哟~

你以为咱们现在开始研究 Operations?哈哈,你又得失望了~ 咱得先看看这两个类:RACEmptySignalRACReturnSignal

##1、两个 RACSignal 的特殊子类 RACEmptySignal 和 RACReturnSignal
###1、RACEmptySignal
RACEmptySignal+[RACSignal empty] 的内部实现,一个私有 RACSignal 子类。它就是一个会立即 completed 的信号。让我们来看看它的 - subscribe: 方法:

 

这样一个订阅者一订阅就会 completed 信号有什么用呢?稍后揭晓。

###2、RACReturnSignal
RACReturnSignal+[RACSignal return:] 的内部实现,也是一个私有 RACSignal 子类。它会同步发送出一个值(即 next)给订阅者,然后再发送 completed 事件。 它比 RACEmptySignal 多了一点点东西,它是。直接看其实现:

纯吐槽:为啥要叫 ReturnSignal 呢?不如直接 OneValueSignal 好了。O(∩_∩)O~~ 不过说真的,RAC 的命名真心不咋地。

那么发送一个 next 后又 completed 的信号又有啥用呢?等下会知道地。

##2、concat: 练手
-[RACSignal concat:] 是源码较简单,且使用频率也较多的。那咱们就来拿它来练练手好了。

RACSerialDisposableRACDisposable 的子类,它包含一个 Disposable,能够在运行时设置这个 Disposable。当设置新的 newDisposable时,老的 oldDisposable 会被 dispose。当 RACSerialDisposabledispose 时,其所包含的 Disposable 会被 dispose

基本上,对一个 RACSignal 的操作的返回值是一个新的 RACSignal 值时,其内部都是调用了 +[RACSignal createSignal:] 这个方法。这个创建信号返回的实际是自定义信号:RACDynamicSignal,针对它前文有所介绍。

这里有一个小技巧。因为很多信号的操作是针对该信号本身 self 所发送的值作的操作。那也就是说会订阅 self,那咱们先找到这一句再说:self subscribe:self subscribeNext:...。嗯,找到了这几行:

在订阅了 self 后,将 nexterror 事件发送给订阅者 subscriber。当 self 发送了 completed 事件事,再让 subscriber 订阅参数 signal。也就是当源信号完成后订阅 signal。怎么样,很简单吧。

##3、zipWith:
再来一个练手的玩意。-[RACSignal zipWith:]-[RACSignal concat:] 稍微复杂点。它是将 self 和 参数 signal 两个信号发送的值合并起来发送给订阅者。

 

同样的,重点在 [self subscriberNext:][signal subscribeNext:] 处。这里的实现是订阅 selfsignal 信号,然后将它们发送出的值收集起来,当两个都发出了值时,分别拿出两个信号最早发出的值,合并为一个 RACTuple,再发送给订阅者 subscriber。这个也很简单吧,只是代码稍多点而已。

##4、bind:
###1、说明
信号的很多 operations 的实现调用来调用去最后都是调用了这个 -[RACSignal bind:] 方法,比如 flattenMap:map:filter 等等。那咱们就来看看这个方法是哪路神仙?

这是在 RACStream 中声明的抽象方法。来看看它的声明:

  RACStreamBindBlock 是一个 block。它从一个 RACStream 中接收一个值,并且返回一个与该流相同类型的实例。如果将 stop 设为 YES,则会在返回一个实例后终结此次 bind。如果返回 nil 则会立即终结。

bind: 方法是将流中每一个值都放到 RACStreamBindBlock 中跑一下。来看看其参数:block。然而这有什么卵用呢?好吧,我太笨,从它的说明来看,我真的不能理解它有什么用。

###2、源码解读
既然从方法说明了解不到,那直接来看其源码了。

我们一步一步来看。先从第 一 步开始,其步骤如下:
1. 订阅 self
  2. 针对 self 发出的每一个值 x,经过 bindingBlock,获取一个信号:signal
    1. 如果 signal 不为 nil,就转到第二步:addSignal
    2. 如果 signal 为 nil,或 stopYES,则转到第三步:completedSignal
  3. 如果 self 发出 error 事件,则中断订阅;如果 self 发出 completed 事件则转到第三步:completedSignal

第二步:addSignal:signal
  1. 先将 signal 添加到 signals
2. 订阅 signal
    1. 将 signalnext 事件转发给订阅者 subscriber
    2. 如果 signal 发送 error 事件则中断订阅
3. 如果 signal 发送 complete 事件,则转到第三步

第三步:completeSignal:signal:disposable
  1. 将 signalsignals 中移除
2. 如果 signals 中没有了 signal,那么订阅就完成了

好了,来总结一下这个 -bind:
1. 订阅原信号 self 的 values。
2. 将 self 发出的任何一个值,都对其使用 bindingBlock 进行转换。
3. 如果 bindingBlock 返回一个信号,则订阅它,将从它那接收到的每个值都传递给订阅者 subscriber
4. 如果 bindingBlock 要求结束绑定,则 complete self 信号。
5. 如果 所有 的信号全都 complete,则给 subscriber 发送 completed 事件.
6. 如果任何一个信号发出 error,将其发送给 subscriber

那从中可以玩出什么花样呢?

###3、示例
咱们先用用它,再看看能怎么玩吧。
####示例1:结合 RACReturnSignal

输出如下:

这个示例就是在 bind: 中简单的返回值。那咱们将这个值变化一下如何?

####示例2:结合 RACReturnSignal、转换 value

输出如下:

哇哇,这就是个 map: 有木有? 现在,有感受到 RACReturnSignal 的魅力?RACReturnSignal-bind: 结合能转换 value。

####示例3:结合 RACEmptySignal
  现在来换个玩法试试看,这回换 RACEmptySignal 来玩玩。

输出如下:

这一次,“outer value” 比 “inner value” 少了一个,这就是 filter: 呀!RACEmptySignalbind: 结合能过滤 value。

####示例4:改进 bind:
  经过这几个示例,我们可以发现,直接使用 bind: 是比较麻烦的。而一般情况下,咱们还真用不到 stop,那咱们就改进一下呗:

哈哈,这个就是 - flattenMap:了。不必过多解释了吧~

##5、-map:
  嗯,这其实就是 -flattenMap:RACReturnSignal 的结合:

##6、-flatten
  信号可以发送任何类型的值,当然也包括 RACSignal 类型。例如,RACCommandexecutionSignals 这个信号,它发出的值就是 RACSignal 类型的。对于这种发出的值是 RACSignal 类型的 RACSignal,叫做 signal of signals。这有点类似于 disposable of disposables。

既然这个信号发出的就是 RACSignal,那在 -flattenMap:中,我们直接将 value 返回就好了。来看看示例:

输出如下:

##7、小结
RACSignal 的 operations 实在太多,全部在这里列出来不现实,也没有这个必要。我相信,经过前面的解析,你现在再去看其它 的一个 operation 源码,也应该不是太大的难事。

#RAC() 宏展开
RAC 的最大的魅力之一就是绑定:RAC(self, ...) = signal; 这应该是大家经常写的一条语句。有没有想过它是怎么工作的呢?咱们来看点代码:

重点在 RAC(self, text) = signal; 这一行。先来看看将这个宏展开是什么样子(RAC 对宏的运用很是牛B,有兴趣请看这篇文章):

看得更清楚一点:

跳到 RACSubscriptingAssignmentTrampoline 类的声明,可以看到:

这个类使用了 clang 的特性,可以使用 []语法([] 的相关文章)。也就是说 assignment[@"text"] = signal;,实际上是这样子的:

再看 - (void)setObject:(RACSignal *)signal forKeyedSubscript:(NSString *)keyPath; 这个方法的实现,我们发现,它其实调用的是 signal 的方法:- (RACDisposable *)setKeyPath:(NSString *)keyPath onObject:(NSObject *)object nilValue:(id)nilValue,再像上面的方法一样来分析这个方法,我们找到了关键点:

哦,原来它就是订阅了 signal,并将 signal 发出的每一值都设置给 objectkeyPath 属性而已。很简单嘛~

#结束
本文研究了 RAC 中的一些基本组件,并没有对一些高级内容进行深入研究,所以才叫“浅析”。但这些也是对高级内容深入研究的基础,既然有“渔”,何惧无“鱼”呢?

其实颇想继续分享,但心有余而力不足。

还可研究的主题:
1. Subjects 它也是 RACSignal 一些操作的基础,值得研究。难度系数:2 (最高为5)
2. RACMulticastConnection 常用,值得研究。难度系数:3
3. Foundation、UIKit、KVO (给各系统类加的 rac_ 扩展),有研究价值。研究过后,你会对 runtime 会有很深入的了解,还会接触到一些 OC 中少用的知识(如 NSProxy 等),能开拓视野。难度系数:5

1 3 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部