iOS Reactivecocoa(RAC)知其所以然(源码分析,一篇足以)

前言

如今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 的使用

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

先上图如下:

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操作流程图

由上图可知,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操作的处理,他们其中的设计与原理也都很巧妙,网上也有很多博客介绍了,这里就暂时不深入探讨了,毕竟写到这里篇幅就已经够长了,大家有兴趣可以自己去看看,以后有时间了我在整理一下!

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容

  • RAC使用测试Demo下载:github.com/FuWees/WPRACTestDemo 1.ReactiveC...
    FuWees阅读 6,355评论 3 10
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 1.ReactiveCocoa常见操作方法介绍。 1.1 ReactiveCocoa操作须知 所有的信号(RACS...
    萌芽的冬天阅读 1,015评论 0 5
  • 前言由于时间的问题,暂且只更新这么多了,后续还会持续更新本文《最快让你上手ReactiveCocoa之进阶篇》,目...
    Karos_凯阅读 1,726评论 0 6
  • 山林伴我度童年,我伴山林扛上肩。亲热之余赢小钱。看昨天,往事如烟飞过川。 又 岸边杨柳问桃林,河里青蛙出水吟...
    木貞ma阅读 899评论 1 3