ReactiveCocoa2 源码浅析

标签(空格分隔): ReactiveCocoa iOS Objective-C


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

  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
 
  难度系数是本人 YY 出来的,别较真,仅当参考。

  顺便打个广告:RAC 交流群:430033580
  

ReactiveCocoa2 源码浅析》有7个想法

    1. 您好 对您博客中订阅者 与 Disposable 的关系中的第二条不太理解, combine的多个信号 其中一个信号completed 那么订阅时候的next 就不接受其他信号的next值了吗

发表评论

电子邮件地址不会被公开。