前言

如今RAC大行其道,对其讲解的博客也多不胜数,稍微有点经验的估计也已经对这个爽到不要不要的框架运用自如了,真正沉下来研究其实现原理的估计也不在少数,这里仅仅是记录一下自己的分析理解,更是在写这篇博客的过程中深化自己对RAC的认知,可能就是想到哪写到哪,各位朋友能从其中学到东西是最好了,要是感觉没什么干货也别对小弟拍砖啊!

一、关于常见类

1、RACSiganl 信号类的使用

如下图:

完成一个信号的生命周期分为四步:

  • 1、创建信号
  • 2、订阅信号
  • 3、发送信号
  • 4、取消订阅(图中未标明)

下面每一步我们细细道来:

1、创建信号

由上面的 信号类使用图可知,创建信号类方法中传入了一个返回值是RACDisposable 类型,参数是遵守 RACSubscriber 协议的吧,名为 didSubscribe 的block,具体实现如下:

由上图可知,内部创建的是一个 RACDynamicSignal 类型的信号,并将 didSubscribe 传入,内部实现如下:

这里就是重点了,首先先创建了一个 RACDynamicSignal 类型的信号,然后将传入的名为 didSubscribe 的block保存在创建的信号的 didSubscribe 属性中,此时仅仅是保存并未触发

一句话总结:创建信号本质就是创建了一个 RACDynamicSignal 类型的信号,并将传入的代码块保存起来,留待以后调用。

2、订阅信号

由上面的 信号类使用图可知,有三种订阅信号的方式,分别是订阅 NextErrorcompleted ,内部实现如下:

首先我们来看第一步,就是创建一个订阅者,并传入相应的block,创建实现如下:

很明显,创建订阅者的实质是,创建一个订阅者,并保存相应的block,比如 neterror、或者complete此时仅仅是保存并未触发!

由上面两图可知,三种订阅方式的流程模式是一致的,仅仅是保存的block不同而已,我们分析一种即可,so 接下来就以 subscribeNext 为例来逐步分析。

接下来,我们看看执行订阅命令这块的实现,如下:

这里,首先我们要知道此处代码实现是在 RACDynamicSignal 里,图中的 didSubscribe 就是第一步创建信号中保存的 didSubscribe block。

由上图可知第一步创建信号中保存的 didSubscribe 代码块在这里执行,并传入了刚刚生成的订阅者(此处的订阅者中保存里 Next block代码块)。

额外的,这里生成了一个 RACCompoundDisposable 类型的disposable,用来管理整个订阅结束及资源的清理,并以传入的订阅者、及当前信号、刚创建的disposable 生成一个 RACPassthroughSubscriber 类型的订阅者,此订阅者仅仅是将传入的三个对象整体包装了一下而已,实质起作用的还是在刚才创建的订阅者,所以,其包含的 next 代码块,依然直接调用即可。

之后,将执行 didSubscribe 代码块返回的 innerDisposable 传入刚刚生成disposable、并将执行此代码块的信号的 schedulingDisposable 也保存到RACCompoundDisposable 类型的disposable中,然后统一管理整个订阅结束及资源的清理。

其中的innerDisposable 就是 上面的 信号类使用图 中表示的第四步,其会在信号结束订阅的时候被调用,做一些清理资源的工作。

而 schedulingDisposable 的话,实际上就是执行代码块放到相应的调度器中,假如没有设置的话,即为 backgroundScheduler,并异步执行 didSubscribe 代码块,假如设置的话就会返回nil,并直接执行代码块,此处嵌套就有点深了,随后讲解取消订阅的时候再细说。

此处用到的RACCompoundDisposable 类型的disposable有点类似于可变数组 NSMutableArray 。而当 RACCompoundDisposable 对象被 disposed 时,它会调用其所包含的所有 disposable 对象的 -dispose 方法。所以可以做统一管理。

一句话总结:订阅信号本质就是创建了一个 RACPassthroughSubscriber 类型的订阅者,并将传入的代码块保存起来,留待以后调用,同时调用了第一步创建信号中保存的代码块,并传入创建的订阅者。

3、发送信号

由上面的 信号类使用图可知,发送同样对应着三种方式,处理如下:

这里有三点需要知道

第一,发送信号就是执行相应block,此处执行的就是第二步中保存的相应的block。

第二,对于 sendErrorsendCompleted 都是先取消订阅,再执行相应的代码块,而 sendNext 并未使订阅结束,这样的话,对之后讨论的各种组合方法中必须写上 sendCompleted来结束订阅的做法就好理解了。

第三,我们也看到三种方法中,假如信号没有相应的block代码块保存,即没有经过第二步去订阅保存代码块,就算是发送了信号也不会执行,此时也就是冷热信号的区别,当然用 RACSubject 来解释更容易理解。

一句话总结:发送信号就是执行订阅信号时对应的block。

4、取消订阅

经过上面三步,我们也了解到了想要结束订阅只要将相应生成的disposable执行dispose即可,那到底为什么呢?现在来讲一讲:

第二步订阅信号中有讲到,执行订阅的代码块实际上是放到相应的调度器中去执行的,如下图:

如上图,是不是很有疑问,假如currentScheduler不为nil的话,那岂不是没有dispose啥事,代码块就直接执行了?我当时也纳闷,不过框架中叙述如下(此处感谢雷神指导,么么哒!):

此时:代码块就不会放到调度器里去执行,而是直接执行,此时disposable的dispose方法就没啥卵用了。

假如currentScheduler为nil的话,会默认一个调度器去管理代码块,具体实现如下:

如图所示,可知在代码块未被调度的之前,生成的disposable被dispose的话,代码块就不会被执行。

一句话总结:取消订阅就是把订阅信号获得的disposable 进行dispose即可在调度器调度该部分代码之前禁止调用。

2、RACSubject 的使用

RACSignal的原理搞清了,接下来就比较好理解了,毕竟信号类都是继承与RACSignal,作用不同也仅仅是部分实现的方式不同而已。

如下图:

1、创建信号

由上图可知,创建RACSubject对象的时候同时创建了相应的一个disposable和一个订阅者数组,没有做其他事情。

一句话总结:创建信号就是额外实例化了一个订阅者数组,没有做其他事情。

2、订阅信号

大致流程与RACSignal是一致的,不同点在下面:

RACSinal订阅的时候直接执行了订阅命令,执行的是创建时保存的代码块,但RACSubject的做法是将订阅者保存到初始化时生成的那个订阅者数组内,此时每个订阅者都包含其对应的代码块(比如:next、error、complete)。

由此可以知道,RACSubject是可以多次订阅的,多次订阅就是把相应包含代码块的订阅者放入订阅者数组内。

另外,此处对于取消订阅做了一些清理工作,将保存的订阅者都移除。

一句话总结:订阅信号就是将代码块保存到订阅者中,并将订阅者添加到信号的订阅者数组中。

3、发送信号

大致流程与RACSignal是一致的,不同点在下面:

由上图可知,RACSubject的发送信号就是遍历自己的订阅者数组,然后分别发送信号。

这也是对月RACsubjec为什么假如有多个订阅者,发送一个信号所有的订阅者都收到的原因。

一句话总结:发送信号就是遍历自己的订阅者数组,然后分别发送信号,并执行该订阅者保存的代码块。

3、其他信号类

还是那句话,信号类都是继承与RACSignal,作用不同也仅仅是部分实现的方式不同而已。

如不同的订阅信号实现:

不同的发送信号实现:

授人以鱼不如授人以渔,知道了这种分析流程方法,其他相应的分析类似,朋友们可以自己试试分析一下,这样你就会发现其实并不是那么难!

4、RACCommand 的使用

这个就比较复杂了,因为他是基于信号的一个对象,里面绑定了很多相关的信号,这里只是讲一讲他的执行原理,及常用的一些方法的分析。

先上图如下:

1、创建命令

由上面 RACCommand流程图 可知创建的时候传入一个返回值为RACSignal类型,参数为id类型的名为input的一个block,具体实现如下:

由上图可知,command的创建实质是额外创建了一个名为 _activeExecutionSignals 的信号数组,并把传入的block保存为 signalBlock 属性。

初始化的时候实际上还有很多其他绑定的代码,就不一一细说了,用到的下面会讲一下。

一句话总结:创建命令就是创建了一个RACCommand类的对象,并将传入的block保存为 signalBlock 属性,然后初始化了一个信号数组,留待以后接收命令信号。

2、订阅命令发出的信号

由上面 RACCommand流程图 可知订阅的信号经过了两步方法,第一步中executionSignals为创建命令时绑定的信号如下:

由上图可知,executionSignals实际上就是最新的第一步创建命令中创建的activeExecutionSignals信号数组。

第二步中的switchToLatest实质上取信号数组的最新的信号。

然后就订阅信号,月RACSignal的订阅流程一样,不再赘述。

一句话总结:订阅命令实质上就是订阅命令保存的信号数组中的最新的信号(目前代码的订阅)

3、判断命令是否执行

与executionSignals类似,在创建命令时也创建了一个信号来监听当前命令是否正在执行。

如上图所示,executing信号实际上是在检测是否有活跃信号,replayLast能确保是最新的值。

一句话总结:判断是否执行命令就是检测是否有活跃信号

4、执行命令

执行命令的话就是传入你所需要传入的对象即可,如下图

由上图,我们可以知道,执行命令先执行了第一步保存的signalBlock并传入接收到的参数,然后把得到的signal加入到第一步创建的 _activeExecutionSignals 信号数组中,留待第二步订阅使用。

一句话总结:执行命令就是将传入的对象传入signalBlock生成signal,并将signal添加到_activeExecutionSignals 信号数组中。

二、关于常用操作方法

1、核心方法bind

在实际开发中我们并不需要直接调用bind方法,但我们所使用API都是以bind方法为基础的,想要了解操作方法的实现原理,bind我们不得不深究一下。

先上 bind 流程图如下:

由上图可知,bind 操作方法的流程如下:

  • 1、创建信号源
  • 2、绑定源信号,生成绑定信号
  • 3、订阅绑定信号
  • 4、发送源信号
  • 5、取消订阅(不再赘述)

由此可知,与正常信号绑定订阅流程不同之处在于,多了一个绑定过程,并且最终订阅的是绑定生成的绑定信号。接下来我们一个一个理一下思路:

1、创建信号源

一句话总结:创建了一个 RACDynamicSignal 类型的信号,并将传入的代码块保存起来,留待以后调用。

2、绑定源信号,生成绑定信号

bind操作流程图可知,bind 方法传入了一个 RACStream * (^RACStreamBindBlock)(id value, BOOL *stop) 的block代码块,其具体实现如下(只截取核心的代码):

由上图可知,bind操作实际上直接返回了一个绑定信号信号,并将 didSubscriber 代码块 传入,保存到返回的信号内。

额外的,绑定信号的 didSubscriber 代码块 中做了两件事

第一,将bind方法传入的block保存为bindinfBlock,留待以后用,并且初始化了一个信号数组。

第二,在代码块内部订阅了源信号,并做了一些处理,如下:

由上面可知,订阅源信号传入的block,被保存在相应订阅者中,留待源信号发送信号触发,内部实现不再赘述。

一句话总结:bind操作实际上是直接生成绑定信号并返回,并且在生成绑定信号传入的didSubscriber block代码块中,保存了bind传入的block,初始化了信号数组,并且订阅了源信号,针对源信号发送信号的流程做了一些处理。(此时未执行,订阅才执行)

3、订阅绑定信号

订阅绑定信号就是实现了第二步中的 didSubscriber 代码块,就是说,第二步仅仅是保存,在订阅它的时候才会实现。

一句话总结:订阅绑定信号就是保存了nextBlock,并且创建订阅者,实现信号的didSubscriber block代码块。

4、发送信号

因为bind比较复杂,所以在此可以将其串起来

1、发送信号触发绑定信号 didSubscriber 代码块中订阅源信号时传入的NextBlock 代码块,如下:

2、是执行第三步中订阅绑定信号保存的从bind传入的额block代码块,并传入由订阅时的传参生成的新的传参,生成returnSignal,具体实现如下:

3、returnSignal内部实现如下

由代码可知,returnSignal实际上是保存了传入的新值,此时就是真正意义上的将源输入做了改变,最后实际输出的就是新保存的值。

4、接下来就是将生成的returnSignal做 addSignal 操作如下:

由上图代码可知,内部实现是直接订阅了传入的returnSignal。

5、之前我们也说过,不同信号对应不同的订阅代码,所以我们来看看returnSignal的订阅时的代码,如下:

由上图代码可知,returnSignal在订阅的时候就同时做了发送信号的任务,即直接就会走订阅保存的代码块,如下:

此处触发的是绑定信号的订阅者发送信号,就是执行绑定信号的 nextBlock,即:

此时就有了运行结果:

ok,bind操作方法就到这了,我已经用尽了我的洪荒之力,么么哒!

一句话总结:bind操作方法实质上就是生成新的绑定信号,利用returnSignal作为中间信号来改变源数据生成新的数据并执行新绑定信号的nextBlock代码块!

2、映射方法 map

所有操作方法都是以bind方法为核心的,这里分析一下映射方法即可,不在赘述其他方法,原理都是一样的,就是看怎么组合,怎么去处理相关逻辑达到不同操作方法达成的效果!

首先,看一下map的操作流程图,如下:

很显然,与 bind 的区别就是生成绑定信号的过程不同,具体实现如下:

由上图可知,从外面传入了一个参数为value,返回值为id类型的block代码块,时机返回的是 flattenMap,操作方法生成的信号,具体实现如下:

看,就是这里,在 map 方法中,返回的是 flattenMap 方法生成的信号,而 flattenMap 内部返回的是 bind 方法返回的信号,又回到了第一部分讲解的 bind实现原理

上图中的 stream,就是returnSignal,即 flattenMap方法传入的 block(value),如下图:

具体生成ReturnSignal的方法,还得往回看 map 方法如下:

由上图可知,传入的value已经被 map保存的方法所映射(即改变),如下:

一句话总结:map 映射方法的实质就是 bind 方法的深度封装,实际的信号流的流程还是以 bind为核心,只是巧妙的做了一些逻辑处理而已。

3、其他操作方法

对于操作方法,核心就是 bind,各种作用的不同就是各种 signalbind 方法的结合,外加一些巧妙的逻辑处理,明白了信号的原理、绑定的原理,任何其他的组合或者运用都是万变不离其宗的,写更多也不如自己去研究一下!么么哒!

所以,操作方法就写到这里!

四、后记

额,这样就差不多了吧,弄明白了本篇所介绍的几个常用的最基本的原理,其实已经对RAC了解的七七八八了,当然RAC还有很多值得去学习的思想,比如RAC宏的使用各种UI操作的处理,他们其中的设计与原理也都很巧妙,网上也有很多博客介绍了,这里就暂时不深入探讨了,毕竟写到这里篇幅就已经够长了,大家有兴趣可以自己去看看,以后有时间了我在整理一下!

本文由作者 王隆帅 编写,转载请保留版权网址,感谢您的理解与分享,让生活变的更美好!

iOS Reactivecocoa(RAC)知其所以然(源码分析,一篇足以)的更多相关文章

  1. 鸿蒙内核源码分析(文件系统篇) | 用图书管理说文件系统 | 百篇博客分析OpenHarmony源码 | v63.01

    百篇博客系列篇.本篇为: v63.xx 鸿蒙内核源码分析(文件系统篇) | 用图书管理说文件系统 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...

  2. 鸿蒙内核源码分析(原子操作篇) | 是谁在为原子操作保驾护航 | 百篇博客分析OpenHarmony源码 | v34.02

    百篇博客系列篇.本篇为: v34.xx 鸿蒙内核源码分析(原子操作篇) | 谁在为原子操作保驾护航 | 51.c.h .o 本篇说清楚原子操作 读本篇之前建议先读鸿蒙内核源码分析(总目录)系列篇. 基 ...

  3. Android事件传递机制详解及最新源码分析——ViewGroup篇

    版权声明:本文出自汪磊的博客,转载请务必注明出处. 在上一篇<Android事件传递机制详解及最新源码分析--View篇>中,详细讲解了View事件的传递机制,没掌握或者掌握不扎实的小伙伴 ...

  4. JUC源码分析-集合篇:并发类容器介绍

    JUC源码分析-集合篇:并发类容器介绍 同步类容器是 线程安全 的,如 Vector.HashTable 等容器的同步功能都是由 Collections.synchronizedMap 等工厂方法去创 ...

  5. JUC源码分析-集合篇(十)LinkedTransferQueue

    JUC源码分析-集合篇(十)LinkedTransferQueue LinkedTransferQueue(LTQ) 相比 BlockingQueue 更进一步,生产者会一直阻塞直到所添加到队列的元素 ...

  6. JUC源码分析-集合篇(九)SynchronousQueue

    JUC源码分析-集合篇(九)SynchronousQueue SynchronousQueue 是一个同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然.SynchronousQu ...

  7. JUC源码分析-集合篇(八)DelayQueue

    JUC源码分析-集合篇(八)DelayQueue DelayQueue 是一个支持延时获取元素的无界阻塞队列.队列使用 PriorityQueue 来实现. 队列中的元素必须实现 Delayed 接口 ...

  8. JUC源码分析-集合篇(七)PriorityBlockingQueue

    JUC源码分析-集合篇(七)PriorityBlockingQueue PriorityBlockingQueue 是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现. P ...

  9. JUC源码分析-集合篇(六)LinkedBlockingQueue

    JUC源码分析-集合篇(六)LinkedBlockingQueue 1. 数据结构 LinkedBlockingQueue 和 ConcurrentLinkedQueue 一样都是由 head 节点和 ...

  10. JUC源码分析-集合篇(四)CopyOnWriteArrayList

    JUC源码分析-集合篇(四)CopyOnWriteArrayList Copy-On-Write 简称 COW,是一种用于程序设计中的优化策略.其基本思路是,从一开始大家都在共享同一个内容,当某个人想 ...

随机推荐

  1. Python 简单统记Log 日记 下次用:python的内置logging模块 easy

    环境 win7  先来new一点log 日记   日记包含    "reason=", "error="  两个log级别 存放在D盘下得LOG目录下 先来 生 ...

  2. UVA - 1606 Amphiphilic Carbon Molecules(两亲性分子)(扫描法)

    题意:平面上有n(n <= 1000)个点,每个点为白点或者黑点.现在需放置一条隔板,使得隔板一侧的白点数加上另一侧的黑点数总数最大.隔板上的点可以看做是在任意一侧. 分析:枚举每个基准点i,将 ...

  3. C++编程学习(四)声明/枚举

    一.typedef 声明 typedef 为一个已有的类型取一个新的名字 typedef int num;//feet是int的另一个名字num a;//a是int类型 二.枚举类型 enum col ...

  4. Sublime Text基本配置

    官网最新版本号 3059 破解链接: http://bbs.pediy.com/showthread.php?t=182774 可以看那个人放出来的百度网盘,去他的百度网盘里面可以看到最新的破解文件. ...

  5. 低JAVA版本,高兼容性启动

    低JAVA版本,高兼容性启动 背景:部分操作系统java环境低版本,暂时无法更新最新版本,新系统需要使用较高版本Java环境 1.JAVA低版本不兼容当前应用 2.解压安装JAVA,无需配置环境变量 ...

  6. WebServerInitializedEvent &ApplicationRunner

    application.properties app.name=yaoyuan2 app.dept.id=1 MyConfig.java import lombok.AllArgsConstructo ...

  7. vue实现CheckBox与数组对象绑定

    实现需求: 实现一个简易的购物车,页面的表格展示data数据中的一个数组对象,并提供选中商品和全选商品checkbox复选框,页面实时显示选中商品的总金额: 分析: 1:使用v-for循环渲染arra ...

  8. Servlet详细教程

    Servlet简介 servlet是Server Applet的简称,翻译过来就是服务程序.好吧,这么说你可能还是不太懂,简单的讲,这个servlet是运行在服务器上的一个小程序,用来处理服务器请求的 ...

  9. MySQL读写分离如何实现?

    主要说下读写分离, 当我们的数据量很大时,数据库服务器的压力变大,这时候我们需要从架构方面来解决这一问题,在一个网站中读的操作很多,写的操作很少,这时候我们需要配置读写分离,把读操作和写操作分离出来, ...

  10. Win10教育版VL版kms密钥激活

    1.右键开始图标,或者win+x,选择Windows PowerShell(管理员): 2.依次执行下面的命令,分别表示安装win10教育版密钥,设置kms服务器,激活win10教育版: slmgr ...