細說 ReactiveCocoa 的冷訊號與熱訊號(三):怎麼處理冷訊號與熱訊號

發表於2015-11-04

第一篇文章中我們介紹了冷訊號與熱訊號的概念,前一篇文章我們也討論了為什麼要區分冷訊號與熱訊號,下面我會先為大家揭曉熱訊號的本質,再給出冷訊號轉換成熱訊號的方法。

揭示熱訊號的本質

ReactiveCocoa中,究竟什麼才是熱訊號呢?冷訊號是比較常見的,map一下就會得到一個冷訊號。但在RAC中,好像並沒有“hot signal”這個單獨的說法。原來在RAC的世界中,所有的熱訊號都屬於一個類——RACSubject。接下來我們來看看究竟它為什麼這麼“神奇”。

在RAC2.5文件的框架概述中,有著這樣一段描述:

A subject, represented by the RACSubject class, is a signal that can be manually controlled.

Subjects can be thought of as the “mutable” variant of a signal, much like NSMutableArray is for NSArray. They are extremely useful for bridging non-RAC code into the world of signals.

For example, instead of handling application logic in block callbacks, the blocks can simply send events to a shared subject instead. The subject can then be returned as a RACSignal, hiding the implementation detail of the callbacks.

Some subjects offer additional behaviors as well. In particular, RACReplaySubject can be used to buffer events for future subscribers, like when a network request finishes before anything is ready to handle the result.

從這段描述中,我們可以發現Subject具備如下三個特點:

  1. Subject是“可變”的。
  2. Subject是非RAC到RAC的一個橋樑。
  3. Subject可以附加行為,例如RACReplaySubject具備為未來訂閱者緩衝事件的能力。

從第三個特點來看,Subject具備為未來訂閱者緩衝事件的能力,那也就說明它是自身是有狀態的。根據上文的介紹,Subject是符合熱訊號的特點的。為了驗證它,我們再來做個簡單實驗:

按照時間線來解讀一下上述程式碼:

  1. 0s時建立subjectreplaySubject這兩個subject。
  2. 0.1s時Subscriber 1分別訂閱了subjectreplaySubject
  3. 0.1s時Subscriber 2也分別訂閱了subjectreplaySubject
  4. 1s時分別向subjectreplaySubject傳送了"send package 1"這個字串作為
  5. 1.1s時Subscriber 3分別訂閱了subjectreplaySubject
  6. 1.1s時Subscriber 4也分別訂閱了subjectreplaySubject
  7. 2s時再分別向subjectreplaySubject傳送了"send package 2"這個字串作為

接下來看一下輸出的結果:

結合結果可以分析出如下內容:

  1. 22.855s時,測試啟動,subjectreplaySubject建立完畢。
  2. 23.856s時,距離啟動大約1s後,Subscriber 1Subscriber 2同時subject接收到了"send package 1"這個值。
  3. 23.857s時,也是距離啟動大約1s後,Subscriber 1Subscriber 2同時replaySubject接收到了"send package 1"這個值。
  4. 24.059s時,距離啟動大約1.2s後,Subscriber 3Subscriber 4同時replaySubject接收到了"send package 1"這個值。注意Subscriber 3Subscriber 4並沒有從subject接收"send package 1"這個值。
  5. 25.039s時,距離啟動大約2.1s後,Subscriber 1Subscriber 2Subscriber 3Subscriber 4同時subject接收到了"send package 2"這個值。
  6. 25.040s時,距離啟動大約2.1s後,Subscriber 1Subscriber 2Subscriber 3Subscriber 4同時replaySubject接收到了"send package 2"這個值。

只關注subject,根據時間線,我們可以得到下圖:

經過觀察不難發現,4個訂閱者實際上是共享subject的,一旦這個subject傳送了值,當前的訂閱者就會同時接收到。由於Subscriber 3Subscriber 4的訂閱時間稍晚,所以錯過了第一次值的傳送。這與冷訊號是截然不同的反應。冷訊號的圖類似下圖:

對比上面兩張圖,是不是可以發現,subject類似“直播”,錯過了就不再處理。而signal類似“點播”,每次訂閱都會從頭開始。所以我們有理由認定subject天然就是熱訊號。

下面再來看看replaySubject,根據時間線,我們能得到另一張圖:

將圖3與圖1對比會發現,Subscriber 3Subscriber 4在訂閱後馬上接收到了“歷史值”。對於Subscriber 3Subscriber 4來說,它們只關心“歷史的值”而不關心“歷史的時間線”,因為實際上12是間隔1s傳送的,但是它們接收到的顯然不是。舉個生動的例子,就好像科幻電影裡面主人公穿越時間線後會先把所有的回憶快速閃過再來到現實一樣。(見《X戰警:逆轉未來》、《蝴蝶效應》)所以我們也有理由認定replaySubject天然也是熱訊號。

看到這裡,我們終於揭開了熱訊號的面紗,結論就是:

  1. RACSubject及其子類是熱訊號
  2. RACSignal排除RACSubject類以外的是冷訊號

如何將一個冷訊號轉化成熱訊號——廣播

冷訊號與熱訊號的本質區別在於是否保持狀態,冷訊號的多次訂閱是不保持狀態的,而熱訊號的多次訂閱可以保持狀態。所以一種將冷訊號轉換為熱訊號的方法就是,將冷訊號訂閱,訂閱到的每一個時間通過RACSbuject傳送出去,其他訂閱者只訂閱這個RACSubject

觀察下面的程式碼:

執行順序是這樣的:

  1. 建立一個冷訊號:coldSignal。該訊號宣告瞭“訂閱後1.5秒傳送‘A’,3秒傳送’B’,5秒傳送完成事件”。
  2. 建立一個RACSubject:subject
  3. 在2秒後使用這個subject訂閱coldSignal
  4. 立即訂閱這個subject
  5. 4秒後訂閱這個subject

如果所料不錯的話,通過訂閱這個subject並不會引起coldSignal重複執行block的內容。我們來看下結果:

參考時間線,會得到下圖:

不難發現其中的幾個重點:

  1. subject是從一開始就建立好的,等到2s後便開始訂閱coldSignal
  2. Subscriber 1subject建立後就開始訂閱的,但是第一個接收時間與subject接收coldSignal第一個值的時間是一樣的。
  3. Subscriber 2subject建立4s後開始訂閱的,所以只能接收到第二個值。

通過觀察可以確定,subject就是coldSignal轉化的熱訊號。所以使用RACSubject來將冷訊號轉化為熱訊號是可行的。

當然,使用這種RACSubject來訂閱冷訊號得到熱訊號的方式仍有一些小的瑕疵。例如subject的訂閱者提前終止了訂閱,而subject並不能終止對coldSignal的訂閱。(RACDisposable是一個比較大的話題,我計劃在其他的文章中詳細闡述它,也希望感興趣的同學自己來理解。)所以在RAC庫中對於冷訊號轉化成熱訊號有如下標準的封裝:

這5個方法中,最為重要的就是- (RACMulticastConnection *)multicast:(RACSubject *)subject;這個方法了,其他幾個方法也是間接呼叫它的。我們來看看它的實現:

雖然程式碼比較短但不是很好懂,大概來說明一下:

  1. RACSignal類的例項呼叫- (RACMulticastConnection *)multicast:(RACSubject *)subject時,以selfsubject作為構造引數建立一個RACMulticastConnection例項。
  2. RACMulticastConnection構造的時候,儲存sourcesubject作為成員變數,建立一個RACSerialDisposable物件,用於取消訂閱。
  3. RACMulticastConnection類的例項呼叫- (RACDisposable *)connect這個方法的時候,判斷是否是第一次。如果是的話_signal這個成員變數來訂閱sourceSignal之後返回self.serialDisposable;否則直接返回self.serialDisposable。這裡面訂閱sourceSignal是重點。
  4. RACMulticastConnectionsignal只讀屬性,就是一個熱訊號,訂閱這個熱訊號就避免了各種副作用的問題。它會在- (RACDisposable *)connect第一次呼叫後,根據sourceSignal的訂閱結果來傳遞事件。
  5. 想要確保第一次訂閱就能成功訂閱sourceSignal,可以使用- (RACSignal *)autoconnect這個方法,它保證了第一個訂閱者觸發sourceSignal的訂閱,也保證了當返回的訊號所有訂閱者都關閉連線後sourceSignal被正確關閉連線。

由於RAC是一個執行緒安全的框架,所以好奇的同學可以瞭解下“OSAtomic*”這一系列的原子操作。拋開這些應該不難理解上述程式碼。

瞭解原始碼之後,這個方法的正確使用就清楚了,應該像這樣:

或者這樣:

以上的兩種寫法和之前用Subject來傳遞的例子都可以得到相同的結果。

下面再來看看其他幾個方法的實現:

這幾個方法的實現都相當簡單,只是為了簡化而封裝,具體說明一下:

  1. - (RACMulticastConnection *)publish就是幫忙建立了RACSubject
  2. - (RACSignal *)replay就是用RACReplaySubject來作為subject,並立即執行connect操作,返回connection.signal。其作用是上面提到的replay功能,即後來的訂閱者可以收到歷史值。
  3. - (RACSignal *)replayLast就是用Capacity為1的RACReplaySubject來替換- (RACSignal *)replay的`subject。其作用是使後來訂閱者只收到最後的歷史值。
  4. - (RACSignal *)replayLazily- (RACSignal *)replay的區別就是replayLazily會在第一次訂閱的時候才訂閱sourceSignal

所以,其實本質仍然是

使用一個Subject來訂閱原始訊號,並讓其他訂閱者訂閱這個Subject,這個Subject就是熱訊號。

現在再回過來看下之前系列文章第二篇中那個業務場景的例子,其實修改的方法很簡單,就是在網路獲取的fetchData這個訊號後面,增加一個replayLazily變換,就不會出現網路請求重發6次的問題了。

修改後的程式碼如下,大家可以試試:

當然,細心的同學會發現這樣修改,仍然有許多計算上的浪費,例如將fetchData轉換為title的block會執行多次,將fetchData轉換為desc的block也會執行多次。但是由於這些block都是無副作用的,計算量並不大,可以忽略不計。如果計算量大的,也需要對中間的訊號進行熱訊號的轉換。不過請不要忽略冷熱訊號的轉換本身也是有計算代價的。

好的,寫到這裡,我們終於揭開RAC中冷訊號與熱訊號的全部面紗,也知道如何使用了。希望這個系列文章可以讓大家更好地瞭解RAC,避免使用RAC遇到的誤區。謝謝大家。

美團iOS組有很多志同道合的小夥伴,對於各種技術都有著深入的瞭解,我們熱忱地歡迎一切牛掰的小夥伴加入,共同學習,共同進步。(簡歷請傳送到郵箱 liangsi02@meituan.com)

相關文章