iOS Reactivecocoa(RAC)知其所以然(源码分析,一篇足以)
前言
如今RAC大行其道,对其讲解的博客也多不胜数,稍微有点经验的估计也已经对这个爽到不要不要的框架运用自如了,真正沉下来研究其实现原理的估计也不在少数,这里仅仅是记录一下自己的分析理解,更是在写这篇博客的过程中深化自己对RAC的认知,可能就是想到哪写到哪,各位朋友能从其中学到东西是最好了,要是感觉没什么干货也别对小弟拍砖啊!
一、关于常见类
1、RACSiganl 信号类的使用
如下图:
完成一个信号的生命周期分为四步:
- 1、创建信号
- 2、订阅信号
- 3、发送信号
- 4、取消订阅(图中未标明)
下面每一步我们细细道来:
1、创建信号
由上面的 信号类使用图
可知,创建信号类方法中传入了一个返回值是RACDisposable
类型,参数是遵守 RACSubscriber
协议的吧,名为 didSubscribe
的block,具体实现如下:
由上图可知,内部创建的是一个 RACDynamicSignal
类型的信号,并将 didSubscribe
传入,内部实现如下:
这里就是重点了,首先先创建了一个 RACDynamicSignal
类型的信号,然后将传入的名为 didSubscribe
的block保存在创建的信号的 didSubscribe
属性中,此时仅仅是保存并未触发。
一句话总结:创建信号本质就是创建了一个 RACDynamicSignal
类型的信号,并将传入的代码块保存起来,留待以后调用。
2、订阅信号
由上面的 信号类使用图
可知,有三种订阅信号的方式,分别是订阅 Next
、Error
、 completed
,内部实现如下:
首先我们来看第一步,就是创建一个订阅者,并传入相应的block,创建实现如下:
很明显,创建订阅者的实质是,创建一个订阅者,并保存相应的block,比如 net
、error
、或者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。
第二,对于 sendError
和 sendCompleted
都是先取消订阅,再执行相应的代码块,而 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
,各种作用的不同就是各种 signal
与 bind
方法的结合,外加一些巧妙的逻辑处理,明白了信号的原理、绑定的原理,任何其他的组合或者运用都是万变不离其宗的,写更多也不如自己去研究一下!么么哒!
所以,操作方法就写到这里!
四、后记
额,这样就差不多了吧,弄明白了本篇所介绍的几个常用的最基本的原理,其实已经对RAC了解的七七八八了,当然RAC还有很多值得去学习的思想,比如RAC宏的使用,各种UI操作的处理,他们其中的设计与原理也都很巧妙,网上也有很多博客介绍了,这里就暂时不深入探讨了,毕竟写到这里篇幅就已经够长了,大家有兴趣可以自己去看看,以后有时间了我在整理一下!
本文由作者 王隆帅 编写,转载请保留版权网址,感谢您的理解与分享,让生活变的更美好!
iOS Reactivecocoa(RAC)知其所以然(源码分析,一篇足以)的更多相关文章
- 鸿蒙内核源码分析(文件系统篇) | 用图书管理说文件系统 | 百篇博客分析OpenHarmony源码 | v63.01
百篇博客系列篇.本篇为: v63.xx 鸿蒙内核源码分析(文件系统篇) | 用图书管理说文件系统 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...
- 鸿蒙内核源码分析(原子操作篇) | 是谁在为原子操作保驾护航 | 百篇博客分析OpenHarmony源码 | v34.02
百篇博客系列篇.本篇为: v34.xx 鸿蒙内核源码分析(原子操作篇) | 谁在为原子操作保驾护航 | 51.c.h .o 本篇说清楚原子操作 读本篇之前建议先读鸿蒙内核源码分析(总目录)系列篇. 基 ...
- Android事件传递机制详解及最新源码分析——ViewGroup篇
版权声明:本文出自汪磊的博客,转载请务必注明出处. 在上一篇<Android事件传递机制详解及最新源码分析--View篇>中,详细讲解了View事件的传递机制,没掌握或者掌握不扎实的小伙伴 ...
- JUC源码分析-集合篇:并发类容器介绍
JUC源码分析-集合篇:并发类容器介绍 同步类容器是 线程安全 的,如 Vector.HashTable 等容器的同步功能都是由 Collections.synchronizedMap 等工厂方法去创 ...
- JUC源码分析-集合篇(十)LinkedTransferQueue
JUC源码分析-集合篇(十)LinkedTransferQueue LinkedTransferQueue(LTQ) 相比 BlockingQueue 更进一步,生产者会一直阻塞直到所添加到队列的元素 ...
- JUC源码分析-集合篇(九)SynchronousQueue
JUC源码分析-集合篇(九)SynchronousQueue SynchronousQueue 是一个同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然.SynchronousQu ...
- JUC源码分析-集合篇(八)DelayQueue
JUC源码分析-集合篇(八)DelayQueue DelayQueue 是一个支持延时获取元素的无界阻塞队列.队列使用 PriorityQueue 来实现. 队列中的元素必须实现 Delayed 接口 ...
- JUC源码分析-集合篇(七)PriorityBlockingQueue
JUC源码分析-集合篇(七)PriorityBlockingQueue PriorityBlockingQueue 是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现. P ...
- JUC源码分析-集合篇(六)LinkedBlockingQueue
JUC源码分析-集合篇(六)LinkedBlockingQueue 1. 数据结构 LinkedBlockingQueue 和 ConcurrentLinkedQueue 一样都是由 head 节点和 ...
- JUC源码分析-集合篇(四)CopyOnWriteArrayList
JUC源码分析-集合篇(四)CopyOnWriteArrayList Copy-On-Write 简称 COW,是一种用于程序设计中的优化策略.其基本思路是,从一开始大家都在共享同一个内容,当某个人想 ...
随机推荐
- Python 简单统记Log 日记 下次用:python的内置logging模块 easy
环境 win7 先来new一点log 日记 日记包含 "reason=", "error=" 两个log级别 存放在D盘下得LOG目录下 先来 生 ...
- UVA - 1606 Amphiphilic Carbon Molecules(两亲性分子)(扫描法)
题意:平面上有n(n <= 1000)个点,每个点为白点或者黑点.现在需放置一条隔板,使得隔板一侧的白点数加上另一侧的黑点数总数最大.隔板上的点可以看做是在任意一侧. 分析:枚举每个基准点i,将 ...
- C++编程学习(四)声明/枚举
一.typedef 声明 typedef 为一个已有的类型取一个新的名字 typedef int num;//feet是int的另一个名字num a;//a是int类型 二.枚举类型 enum col ...
- Sublime Text基本配置
官网最新版本号 3059 破解链接: http://bbs.pediy.com/showthread.php?t=182774 可以看那个人放出来的百度网盘,去他的百度网盘里面可以看到最新的破解文件. ...
- 低JAVA版本,高兼容性启动
低JAVA版本,高兼容性启动 背景:部分操作系统java环境低版本,暂时无法更新最新版本,新系统需要使用较高版本Java环境 1.JAVA低版本不兼容当前应用 2.解压安装JAVA,无需配置环境变量 ...
- WebServerInitializedEvent &ApplicationRunner
application.properties app.name=yaoyuan2 app.dept.id=1 MyConfig.java import lombok.AllArgsConstructo ...
- vue实现CheckBox与数组对象绑定
实现需求: 实现一个简易的购物车,页面的表格展示data数据中的一个数组对象,并提供选中商品和全选商品checkbox复选框,页面实时显示选中商品的总金额: 分析: 1:使用v-for循环渲染arra ...
- Servlet详细教程
Servlet简介 servlet是Server Applet的简称,翻译过来就是服务程序.好吧,这么说你可能还是不太懂,简单的讲,这个servlet是运行在服务器上的一个小程序,用来处理服务器请求的 ...
- MySQL读写分离如何实现?
主要说下读写分离, 当我们的数据量很大时,数据库服务器的压力变大,这时候我们需要从架构方面来解决这一问题,在一个网站中读的操作很多,写的操作很少,这时候我们需要配置读写分离,把读操作和写操作分离出来, ...
- Win10教育版VL版kms密钥激活
1.右键开始图标,或者win+x,选择Windows PowerShell(管理员): 2.依次执行下面的命令,分别表示安装win10教育版密钥,设置kms服务器,激活win10教育版: slmgr ...