iOS Reactivecocoa(RAC)知其所以然(原始碼分析,一篇足以)

王隆帥發表於2016-09-03

前言

如今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操作的處理,他們其中的設計與原理也都很巧妙,網上也有很多部落格介紹了,這裡就暫時不深入探討了,畢竟寫到這裡篇幅就已經夠長了,大家有興趣可以自己去看看,以後有時間了我在整理一下!

本文由作者 王隆帥 編寫,轉載請保留版權網址,感謝您的理解與分享,讓生活變的更美好!

相關文章