天天看點

走進ReactiveCocoa的世界

​<code>​ReactiveCocoa​</code>​ 是一套開源的基于Cocoa的FRP架構 .FRP的全稱是​<code>​Functional Reactive Programming​</code>​,中文譯作函數式響應式程式設計,是RP(Reactive Programm,響應式程式設計)的FP(Functional Programming,函數式程式設計)實作。說起來很拗口。太多的細節不多讨論,我們先關注下FRP的FP特征。

函數式程式設計,簡單來說,就是多使用匿名函數,将邏輯處理過程,以一系列嵌套的函數調用來實作,以減少中間狀态的存在。

簡單舉例,一個簡單的表達式,(将表達式了解成一系列的邏輯處理流程):

1

2

如果是傳統的過程式程式設計,則會這樣寫:

3

4

而在函數式程式設計中,我們将運算過程(邏輯處理流程),定義為不同的函數,然後會寫成:

從這裡,我們就可以看出一個特點,過程式程式設計會在運作中,将一步步操作的結果以狀态的形式紀錄下來,下一步操作是修改上一步操作的結果。 而在函數式程式設計中,上一步操作的結果會直接以參數的形式或者其它形式傳遞給下一步操作,不再本地儲存一堆無用的中間狀态,而是輸入一個初始值,就傳回一個相應的結果。中間狀态會互相影響,過多的中間狀态會降低代碼可讀性以及提高維護的難度。通過函數式程式設計,減少狀态的存在,一個操作,一個流程,隻由輸入值來決定輸出結果,不在運作過程中以來全局狀态或者儲存中間狀态。 是以函數式程式設計的主要優點就在于 不儲存中間狀态,缺點的話,為了不儲存這個中間狀态,而在函數間傳遞,會增加函數的調用次數,而這樣會在一定程度上降低效率。

函數式程式設計的其它優點:

代碼簡潔,開發快速。

接近自然語言,易于了解。

利于單元測試,和子產品化組合。

易于并發程式設計

函數式程式設計,指盡量減少狀态的儲存,直接由輸入得到結果,而不是在一些地方放置一堆的狀态.即Model更新時,是直接作用于View,讓View做出相應的顯示,而不是儲存一個狀态,然後再通知View來擷取這個狀态.對于少量的狀态,這樣處理起來可能沒問題,但是一旦狀态多起來,管理就變得十分麻煩,難以調試.是以函數式程式設計,目的是 讓相同的輸入導出相同的輸出,減少由于儲存狀态帶來的影響.

響應式程式設計是一種面向資料流和變化傳播的程式設計範式。這意味着可以在程式設計語言中很友善地表達靜态或動态的資料流,而相關的計算模型會自動将變化的值通過資料流進行傳播。 

例如,在指令式程式設計環境中,a:=b+c表示将表達式的結果賦給a,而之後改變b或c的值不會影響a。但在響應式程式設計中,a的值會随着b或c的更新而更新。

響應式程式設計對應的是指令式程式設計,指令式程式設計中,資料以狀态的形式儲存,通過指令來通知需要狀态的對象來更新狀态。iOS和UIKit在設計上是指令式的,如TableView的DataSource ,委托模式強制将狀态儲存在委托中,以在請求發生時,為TableView提供資料.

資料的改變導緻View的改變,現在我們常用的一般是指令式的程式設計,控制器去發動資料的改變,然後儲存資料的狀态,然後再去驅動View來加載最新的資料狀态.

随着業務變得更加複雜,這種做法導緻Controller上有一堆的狀态資料,而這些狀态資料錯綜複雜,互相影響,導緻代碼可讀性較差,是以我們傾向于使用一種函數式程式設計的方式.資料修改後,直接設定View,而不是儲存資料的狀态,或者說将這步資料狀态儲存的操作抽離出來,不放在業務代碼中,以提升代碼的可讀性,這就是使用​<code>​ReactiveCocoa​</code>​的原因.

​<code>​ReactiveCocoa​</code>​ 是一種​<code>​Funcational Reactive Programming​</code>​。 不是用一堆表示狀态的變量,這些變量會在許多情況下被修改,而是使用一種事件流的方式,用​<code>​Signal​</code>​信号來表示狀态,實時傳遞狀态。

事件流已經統一了Cocoa中的常用的異步和實作處理,如:

Delegate methods

Callback blocks

NSNotifications

Control actions and responder chain events

Futures and promises

Key-value observing (KVO)

将這些事件處理統一成一種信号的形式,是以就可以聲明鍊式處理和聚合信号。

總結,很多iOS程式,對于事件的處理和響應是基于應用狀态的,随着回調的增加和狀态變量的增加,處理這些事件的代碼就回變得異常複雜,這就是我們使用ReactiveCocoa的原因,通過響應式的處理以消除中間狀态,增加代碼可讀性和擴充性,降低複雜度。

RAC通過一堆Block來實作事件流的傳遞,是以調試是意見很可怕的事情,在信号進行中的斷點,會發現整個堆棧上全是RAC自己處理信号發送信号的操作,很難找到有用的資訊,找到目前斷點的上一級觸發者。

RAC中推薦通過在事件流中加入副作用來進行調試。​<code>​.on(event: { print ($0) })​</code>​,也可以通過​<code>​logEvents​</code>​來輸出事件流,但是這是2.5以後添加的新接口,而我們目前使用的依舊是2.5版本,因為2.5版本是純Obejctive-C的。

​<code>​ReactiveCocoa​</code>​是由​<code>​Reactive Extensions​</code>​(微軟的Rx庫)啟發,并在之後也深深地受其影響的。而​<code>​RxSwift​</code>​是RX的正式成員,

​<code>​ReactiveCocoa​</code>​是受FRP啟發,但開發環境是在Cocoa上,是以API更接近于Cocoa。

主要差別在于RAC中有冷熱信号的概念,這是RAC中的一個核心特點,後面會介紹到。RX則把冷熱信号統一。

目前我們主要是在OC上開發,以後OC和Swift混編,這些情況下使用RAC都會更加友善一點。

Overview

在RAC的世界中,使用信号流處理事件. 信号的發送者,稱為 ​<code>​receiver​</code>​,信号的接收者,稱為​<code>​subscriber​</code>​.

Stream,由​<code>​RACStream​</code>​抽象類表示,表示一個對象的一系列的值。

值可以立刻被拿到,也可以在未來被拿到,但收到值是順序的。不可能在流上不接受到第一個值而直接擷取第二個值。

Stream是​<code>​monads​</code>​的(在FP中表示用步驟流程表示的運算操作的結構),基于簡單的基礎信号進行複雜的操作。

​<code>​RACStream​</code>​是一個抽象基類,通過​<code>​signal​</code>​和​<code>​sequences​</code>​來實作。

信号由​<code>​RACSignal​</code>​類表示。信号表示那些将要在未來被傳遞的資料。程式運作時,資料的值在信号中被傳遞,推送給​<code>​Subscribers​</code>​。使用者需要去訂閱這些信号擷取資料。

信号發送三種不同的事件:

next : 傳遞資料流中的新的值。​<code>​RACStream​</code>​的操作方法隻能處理next類型的事件。傳遞的資料可以為nil。

error : 表示信号完成前發生了錯誤。事件包含一個​<code>​NSError​</code>​的對象來表示具體錯誤。必須特别處理,因為錯誤并不包含在​<code>​RACStream​</code>​中。

completed : 表示信号完成,不會再有新的值被添加到​<code>​Stream​</code>​中。也需要特别處理,事件不會被包含在​<code>​Stream​</code>​中。

next的事件可以有任意數量個,但是​<code>​error​</code>​和​<code>​completed​</code>​事件最多隻會發生一個。

​<code>​subscriber​</code>​:訂閱者,訂閱信号,等待處理信号所發送的事件。在RAC中,一個訂閱者者指繼承​<code>​RACSubscriber​</code>​協定。

一個訂閱的建立可以通過調用​<code>​-subscribeNext:error:completed:​</code>​等方法。訂閱者持有所訂閱的信号,會在信号完成或出錯的時候自動釋放,當然也可以手動釋放​<code>​Subscription​</code>​。

​<code>​Subject​</code>​ 用​<code>​RACSubject​</code>​類表示,是一個可以手動管理的信号,也就是我們要着重讨論的熱信号。

​<code>​Subject​</code>​可以了解為一個信号的​<code>​mutable​</code>​版本。

不在block中處理應用邏輯,而是将這些block發送給一個共享的Subject來處理。​<code>​RACSubject​</code>​是​<code>​RACSignal​</code>​的子類。

​<code>​RACReplaySubject​</code>​可以緩存Event,供以後的訂閱者進行監聽。

​<code>​RACCommands​</code>​類,建立和訂閱一個信号,并監控其狀态。是對信号的封裝,将一個信号的狀态,以​<code>​executionSignals​</code>​表示封裝的信号,用​<code>​executing​</code>​表示信号執行的狀态,用​<code>​enabled​</code>​表示信号是否可用,用​<code>​errors​</code>​表示信号執行中的異常。

這個屬性通常與UI控件結合在一起,用​<code>​enabled​</code>​信号來控制控件是否可用,用​<code>​executionSignals​</code>​來表示控件可用時要執行的操作。

​<code>​RACMulticastConnect​</code>​類,表示可以在任意數量的訂閱者中共享的一個信号。即是我們重點要讨論的第二個問題,冷熱信号。

信号預設是冷的,即每當一個新的訂閱者添加的時候,他們才開始處理事件,發送信号。這是一件可取的做法,資料會在每次訂閱的時候重新整理。但是對于有副作用的信号,或者操作消耗太多資源的信号(如網絡請求),顯然是有問題的。尤其是在RAC中,每次都信号進行的邏輯處理操作都是在訂閱前一個信号。

通過RACSignal上的​<code>​pulish​</code>​或者​<code>​multicast:​</code>​方法建立一個這樣的熱信号。

​<code>​RACSequence​</code>​表示表示一組信号,類似于​<code>​NSArray​</code>​。​<code>​RACSequence​</code>​表示一些列的信号,其有兩個主要屬性,id類型的​<code>​head​</code>​,和​<code>​RACSequence​</code>​類型的​<code>​tail​</code>​,則周遊這個清單時類似于一種遞歸的方式。則就展現了​<code>​RACSequence​</code>​的懶加載特性,如果這組​<code>​Sequences​</code>​中的值沒有被使用,那就不會去擷取這個值。

一般用于周遊數組 :

5

6

7

8

9

10

​<code>​RACDisposables​</code>​表示對信号的取消操作和資源的釋放操作。用于取消信号的訂閱,釋放信号。一般網絡操作和背景處理的一些耗時操作,都應該提供​<code>​RACDisposables​</code>​。調用​<code>​RACDisposables​</code>​的​<code>​dispose​</code>​方法,以取消正在訂閱中的信号. 而當信号執行完成時,也會調用​<code>​RACDisposables​</code>​.

​<code>​RACScheduler​</code>​,提供一系列的執行隊列,供信号按需執行操作或者發送結果。

​<code>​RACScheduler​</code>​類似于​<code>​GCD​</code>​,但是提供了取消功能,通過​<code>​disposables​</code>​,而且隻執行串行任務。對于使用​<code>​immediateScheduler​</code>​建立的scheduler,不支援使用同步方法。可以看到設計是在通過一些限制來避免死鎖的發生。

RAC提供了一些類在Stream中傳遞值。

RACTuple : 一個簡單的固定大小的集合,可以包含nil對象(用​<code>​RACTupleNil​</code>​對象表示)。一般用于表示多個信号聚合時,聚合信号傳遞的資料的值。

RACUnit : 代表一個空值對象。

RACEvent : 代表信号的事件,即next,error,completed三種事件。通過​<code>​materialize​</code>​方法,将三種信号合成一種發送給訂閱者來統一處理。

Basic Operators

介紹幾個簡單的基礎的操作符.

使用 ​<code>​subscribe​</code>​指令來根據信号的目前或未來的值設定響應操作:

在冷信号中,每次訂閱信号都會執行副作用.

使用 ​<code>​do...​</code>​指令,在不訂閱信号的情況下,添加副作用.

11

12

13

14

15

16

17

18

19

20

​<code>​do​</code>​的操作會比 ​<code>​subscribe​</code>​要先執行.

這些操作,将一個信号流轉變為一個新的信号流.

使用​<code>​map:​</code>​指令,将信号的值進行替換.

使用​<code>​filter:​</code>​指令,過濾信号值,過濾NO的信号值:

将多個信号流聚合成一個信号流

使用 ​<code>​concat:​</code>​指令,将一個信号的值接在另一個信号後面:

使用​<code>​flatten:​</code>​指令,對于信号中的信号,将 信号的值整合進一個新的信号流. 如下,連接配接​<code>​Sequence​</code>​時:

這裡 ​<code>​sequenceOfSequences​</code>​,這個信号的值 是一個信号,使用​<code>​flatten​</code>​,将信号中的信号的值 給取出來,作為信号的值,建立一個新的信号.

再聚一個聚合信号的例子:

21

22

使用​<code>​flatten​</code>​的目的,一般并不是為了這個效果,而是為了操作​<code>​flattenMap:​</code>​.

​<code>​flattenMap:​</code>​指令,用于轉換多個信号流中的信号值輸出一個新的信号流.從字面意思上來解釋,這個操作就是, 先​<code>​map​</code>​操作,處理信号,然後再​<code>​flatten​</code>​操作,将信号中的信号提取出來作為一個完整的信号. 也就是說,block中return的是一個信号,而​<code>​flattenMap​</code>​傳回值是一個信号,将return中的信号​<code>​flatten​</code>​後的一個完整的信号.還是用​<code>​sequence​</code>​來舉例:

​<code>​flattenMap​</code>​也用于将複數個信号的工作自動的結合在一起:

将多個信号聚合成一個信号.

使用​<code>​then:​</code>​指令, 一般為 ​<code>​signalA then:^{return signalB}​</code>​,表示 訂閱​<code>​signalA​</code>​的信号,但是忽略所有的​<code>​next​</code>​事件,當​<code>​completed​</code>​事件發送時,訂閱B信号并傳回B信号的事件.

使用場景, 執行完前一個信号的所有副作用,然後開始另一個新信号,将其傳回值作為真正的信号值 傳遞.

​<code>​merge:​</code>​指令,如名字一樣,合并信号,但是,根據的是信号值到來的順序:

​<code>​combineLastest:​</code>​和​<code>​combineLastest:reduce:​</code>​兩個方法,用來聚合信号,當多個信号中的任何一個信号發生變化時,都會取每個信号的最新值,組合成一個新的值發送.而兩個方法的差別在于,前者傳遞的信号是一個​<code>​RACTuple​</code>​對象,将多個信号的值封裝在一個對象中,而後者在​<code>​reduce​</code>​的block中來處理多個信号的值,将其整合成一個值傳回.

一定要注意這裡,聚合的所有信号都有第一個值後,這個聚合信号才會發送第一個值,是以這裡​<code>​letters​</code>​發送的第一個信号​<code>​A​</code>​,不會在聚合信号​<code>​combined​</code>​中出現.

然後就要與另外一個方法​<code>​zip​</code>​進行比較.

壓縮信号.與上面的​<code>​combineLastest​</code>​有些相似,但是差別在于, ​<code>​zip​</code>​需要等待多個信号都有一個最新的值後,才會發送一個信号,而​<code>​combine​</code>​中,任何一個信号有新的值後,都會發送信号.

舉例,還是類似​<code>​combineLatest​</code>​中的例子,隻是将​<code>​combineLatest​</code>​改為​<code>​zip​</code>​,但是結果就變了.

在這裡,最終輸出地結果是​<code>​A1​</code>​,​<code>​B2​</code>​,​<code>​C3​</code>​.

使用​<code>​switchToLastest​</code>​來擷取信号中的信号的值,傳遞​<code>​next​</code>​和​<code>​error​</code>​,但是不傳遞​<code>​complete​</code>​:

23

當信号中的信号發送​<code>​complete​</code>​時,由于​<code>​switchToLastest​</code>​不接收這個​<code>​completed​</code>​事件,是以 ​<code>​switchToLastest​</code>​的信号依舊繼續處理.但是當信号中的信号發送​<code>​error​</code>​時,會中斷​<code>​switchToLastest​</code>​的信号處理:

24

25

26

27

上面這段代碼示例中,輸出結果為 :

信号A發送完成後,AA的信号就無法發送給​<code>​signalOfSignals​</code>​了,但是一個新的信号B還是能夠繼續發信号給​<code>​signalOfSignals​</code>​的.但當信号B發送error後,這個​<code>​signalOfSignals​</code>​會接受到這個error,進而結束訂閱.

Design Guidelines

在​<code>​RACSequence​</code>​中擷取值,預設是延遲計算的:

隻有真正使用時,才會進行計算,擷取到真正的值;如通過​<code>​sequence.head​</code>​才會去計算出​<code>​A_​</code>​的值。

而且,隻會計算一次,即多次通路​<code>​sequence.head​</code>​,但​<code>​[str stringByAppendingString:@"_"]​</code>​操作隻會執行一次。

如果不需要這種延遲加載,而需要在以來開始的時候初始化整個數組,那就使用​<code>​eagerSequence​</code>​這個屬性。

計算操作是同步執行的,這需要注意一下。如果數組的計算操作是比較耗費時間的,可以通過接口​<code>​signalWithScheduler:​</code>​來在一個隊列中執行數組計算操作,并擷取完成信号。

對于​<code>​RACSequence​</code>​進行運算時,因為所做的運算一般是求得一個新的​<code>​RACSequence​</code>​,而​<code>​RACSequence​</code>​隻會在第一次使用到值時才會進行計算:

信号事件是連續的串行的。一個信号可以分發事件到任何一個線程,連續的事件可以被選擇在不同的線程或者​<code>​schedulers​</code>​.但是有些情況會要求事件必須在特殊的​<code>​scheduler​</code>​上執行,如UI操作,就必須在主線程上進行。

RAC保證,不會有兩個信号同時到達,是以不會有兩個信号的事件同時在一個線程上被激活的可能。即在RAC中,當一個事件在處理過程中,不會有其他事件被分發。隻有當事件被處理完成後才會有新的事件發送出去。

這就意味着 : 在​<code>​-subscribeNext:error:completed:​</code>​回調中,不需要對變量進行加鎖操作,因為事件處理全部是串行的。

訂閱操作,始終執行在一個​<code>​schedular​</code>​上。為了確定​<code>​createSignal​</code>​和​<code>​subscribe​</code>​方法執行的一緻性表現,RAC會保證 消息操作的執行和 消息訂閱的操作會在同一個​<code>​scheduler​</code>​中。

如果在訂閱時,執行代碼​<code>​+[RACScheduler currentScheduler]​</code>​無法獲得一個​<code>​RACScheduler​</code>​時,會将訂閱和消息放在一個背景的​<code>​RACScheduler​</code>​中執行。如果能夠得到一個​<code>​RACScheduler​</code>​則會在目前​<code>​RACScheduler​</code>​中執行。

再說明一下這個​<code>​[RACScheduler currentScheduler]​</code>​,這個函數會再 ​<code>​RACScheduler​</code>​中傳回正确的​<code>​Scheduler​</code>​,以及在主線程中傳回​<code>​+[RACScheduler mainThreadScheduler]​</code>​. 是以上面一段話的意思,就是如果在主線程或者在執行一個​<code>​RACScheduler​</code>​中,訂閱會發生在這個​<code>​Scheduler​</code>​中,否則會發生在一個背景的​<code>​Scheduler​</code>​中。

在RAC中,​<code>​error​</code>​在語義上表示異常。當信号中發生一個Error信号時,會立即發送給所有相關的信号,并使整個消息鍊終止。

但這并表示 ,對于​<code>​Error​</code>​的處理的操作符 ,如​<code>​catch:​</code>​ ​<code>​catchTo:​</code>​ 和​<code>​materialize​</code>​ 這幾個錯誤處理也會終止。

每一次對信号的訂閱,都會觸發副作用。原因很簡單,因為這些信号是冷信号,冷信号會在訂閱時執行。而需要注意的是,所有對信号的操作,都是訂閱信号,并發送新的信号。

想要取消這種效果,那就是用熱信号吧。

再次說明一下,冷信号的副作用效果,會産生許多問題,且難以發現,一定要注意。要了解冷熱信号的差別。

當一個消息發送了​<code>​completed​</code>​或​<code>​error​</code>​事件時,這個​<code>​subscription​</code>​會自動被釋放。節省手動釋放的操作。

而釋放信号時,要對那些 檔案操作或者網絡操作等,進行資源釋放和過程中斷。

Best practices

當一個方法或者屬性傳回一個​<code>​RACSignal​</code>​類型的信号時,很難很快地了解一個信号的含義。

對于聲明一個信号,有以下三個關鍵性的問題:

信号是熱信号還是冷信号?

信号有一個值還是沒有值還是多個值?

信号有副作用嗎?

熱信号且沒有副作用 ,這種情況應該将信号作為一種屬性。使用屬性,表明對信号的訂閱不需要進行初始化,而且添加新的訂閱也不會改變這個用法。信号的屬性一般被命名為 ​<code>​名字 + 事件​</code>​ ,如 ​<code>​textChanged​</code>​.

冷信号且無副作用 , 這種情況應該作為一個函數,且命名使用一個名詞來表示,如​<code>​currentText​</code>​. 一個名詞的函數聲明,表示了這個信号不會被一直持有,同時聲明操作是發生在訂閱時.如果信号發送了複數個值,需要在命名時表明這一點,如​<code>​currentModels​</code>​

有副作用的信号, 信号應該是以方法形式傳回,并表示動作,如​<code>​logIn​</code>​. 動詞表明了這個函數不是靜态的,調用者要小心調用時的副作用. 如果信号會發送一個或者多值,應該要再命名中表明值的含義,如​<code>​loadConfiguration​</code>​和​<code>​fetchLastestEvents​</code>​.

使用RAC書寫代碼時,在處理信号中得操作流很容易變得很重很多,大量的操作符與block聚集在一起,如果沒有進行很好地格式化,那這段代碼就将變得亂七八糟.是以,建議,在流的處理過程中,對操作符進行縮進 :

在一個流中,使用一種類型來作為各個過程的信号值.雖然RAC中支援使用任何類型的值作為信号值來傳遞,但是在一個完整地流中,使用多種不同類型的值,會導緻代碼可讀性降低,也會增加訂閱者的負擔,必須更加小心地去處理這個奇怪的信号.

不要持有​<code>​RACStream​</code>​對象過長時間.持有一個​<code>​RACStream​</code>​對象的同時,也會導緻以來這個​<code>​RACStream​</code>​對象的所有對象都被持有,無法正常釋放,這将降低記憶體使用率.

例如 :一個​<code>​RACSequence​</code>​對象在需要使用其​<code>​head​</code>​屬性時,可以持有這個對象,但當不再使用​<code>​head​</code>​時,就應該抛棄這個​<code>​RACSequence​</code>​了,如果需要之後的資料,可以持有其​<code>​tail​</code>​屬性而不是持有這個​<code>​RACSequence​</code>​本身.

保持一個​<code>​Stream​</code>​或者​<code>​RACSignal​</code>​的訂閱,會浪費性能和記憶體,如果一個信号的結果不需要使用,就應該丢棄這些信号.

我們可以使用​<code>​take:​</code>​和​<code>​takeUntil:​</code>​等方法,來做這種判斷邏輯.這個方法會在邏輯判斷不會在接收消息時,取消該信号訂閱的堆棧,終止所有的依賴項的訂閱.

可以将信号使用​<code>​deliverOn:​</code>​在一個指定的​<code>​Scheduler​</code>​上發送事件,如對于一些UI的操作,可以聲明其在主線程中執行. 這個指令指的是subscribe的操作在指定的​<code>​Scheduler​</code>​中執行,但是副作用還是在原始的線程中執行.

但是,盡量少得去切換​<code>​Scheduler​</code>​,線程間的切換,會有不必要的延遲出現,而且會消耗CPU的性能. 是以​<code>​deliverOn:​</code>​的操作,一般放在信号鍊的最後一級執行.

明确地說明一個信号有副作用. 我們應該避免信号的副作用,因為我們很難控制副作用的發生.

但這種場景還是需要的,是以RAC中提供了​<code>​doNext:​</code>​ ​<code>​doError:​</code>​和​<code>​doCompleted​</code>​三個方法來提供明确地副作用的處理.

在熱信号中,分享副作用.使用​<code>​publish​</code>​和​<code>​multicast​</code>​兩個指令來讓一個信号釋出成一個熱信号,變成​<code>​RACMulticastConnection​</code>​對象.

每個​<code>​RACStream​</code>​都有一個屬性​<code>​name​</code>​,來用于調試.而一個​<code>​Stream​</code>​的​<code>​description​</code>​的中會自動包含所有操作的列舉出來.

如上面列印出來的結果是 :​<code>​[[[RACObserve(self, username)] -distinctUntilChanged] -take: 3] -filter:​</code>​

可以通過​<code>​setNameWithFormat​</code>​來設定一個signal最開始的名稱.​<code>​RACSignal​</code>​也提供了​<code>​logNext​</code>​,​<code>​logError​</code>​,​<code>​logCompleted​</code>​和​<code>​logAll​</code>​這些方法,可以自動的在事件發生時打日志.

避免明确地 訂閱和釋放操作.而使用以下幾個方法:

使用​<code>​RAC()​</code>​和​<code>​RACChannelTo()​</code>​宏,來綁定一個信号到對應的屬性,而不是聲明一個手動的改動來執行操作.

使用​<code>​rac_liftSelector:withSignals:​</code>​方法,當信号發生時會調用​<code>​Selector​</code>​.

像​<code>​takeUntil:​</code>​這樣的方法,用來自動的釋放一次訂閱.

盡量使用RAC提供的方法來操作信号來導出一個正确的符合效果的信号流,供訂閱.

避免使用​<code>​Subjects​</code>​. ​<code>​Subjects​</code>​是一個強力的工具,橋接代碼與信号.但是過度使用會導緻代碼變得更加複雜.盡量少的使用熱信号,建議:

對于要給信号中得值初始化的情況,使用​<code>​createSignal:​</code>​的block進行初始化操作.

對于分發一個中間的結果給​<code>​subject​</code>​的情況,改用 ​<code>​combineLatest:​</code>​或​<code>​zip:​</code>​方法來合并多個信号來實作.

對于想要做出一個熱信号供多個對象訂閱的情況,通過​<code>​multicast​</code>​一個基礎的信号來解決.

instead of implementing an action method which simply controls a subject,use a ​<code>​RACCommand​</code>​ or ​<code>​rac_signalForSelector​</code>​ instead.

如果要使用​<code>​subjects​</code>​,應該将其作為一個信号鍊的基礎,而不是中間的一個環節.

Memory Management

RAC中得記憶體管理是很複雜的,但是這樣做的目的是,使用者不用持有信号來驅動信号發送的過程.

除了那些會長期存在的信号,會被以屬性的形式持有,一般不要去持有信号.

信号的建立後,會自動的添加到一個全局的信号集合中.

然後這個信号會等待一次 main run loop的執行,這時如果信号還是沒有被訂閱,那信号将被集合移除.如果信号沒有在其他地方被持有,那信号将會在這個時候被釋放.

如果信号被訂閱了,那信号會在這個信号集合中被持有.

如果信号的所有訂閱者都消失了,那會重複第二步操作,即再等待一個runloop周期.

當使用​<code>​subscribeNext:error:completed:​</code>​訂閱信号時,隐式地建立了一個​<code>​RACSubscriber​</code>​對象.是以建立信号時使用的block所關聯的對象會被訂閱所持有.

在RAC的記憶體關聯中,一個重要的注意事項就是, 訂閱會在 completion 或是error時終止,訂閱者也會被移除.

這樣,信号的生命周期也就會跟随事件流的邏輯生命周期.

會有一些不會自行結束的信号存在,是以需要​<code>​disposable​</code>​存在.

信号訂閱的​<code>​dispose​</code>​操作,會移除所有關聯的訂閱者,而且也會釋放該信号所占有的資源.

有些信号是有self衍生出來的.如 ​<code>​RACObserve()​</code>​監聽​<code>​self​</code>​的一個屬性時,在​<code>​subscribeNext​</code>​使用self指針,就會形成一個引用環.

建議使用​<code>​@weakify​</code>​和​<code>​@strongify​</code>​這兩個宏來處理指針. 當對象不能使用weak時,使用 ​<code>​__unsafe_unretained​</code>​ 或 ​<code>​@unsafeify​</code>​.

但很多時候,有一種更好地寫法來解決循環指針的問題,如對于一般寫法:

實際上我們可以這樣寫:

或者這樣寫 :

冷熱信号詳解

這裡的内容主要是在學習 美團的技術分享文章,裡面幾張圖也是直接從這裡拿過來的.

然後我們再來讨論這個RAC中一個重點問題.也就是冷熱信号.

冷熱信号的起源來自于RX的​<code>​Hot Observable​</code>​和​<code>​Cold Observable​</code>​,兩者的差別是:

熱信号是主動的,即使你沒有訂閱事件,它仍然會時刻推送。

熱信号可以有多個訂閱者,是一對多,信号可以與訂閱者共享資訊

産生熱信号的原因,有些信号我們不想再訂閱時就執行一次,而是全局共享一個信号.如網絡請求的信号,我們并不希望在每次對網絡請求結果信号進行訂閱時,就執行一次新的網絡請求.是以我們要将信号轉換為熱信号,以隻執行一次網絡通路,而結果可以供多次訂閱共享.

熱信号都屬于一個類​<code>​RACSubject​</code>​,這個類在RAC中表示一個可變的信号.我們寫一段代碼來示範一下其效果:

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

輸出的結果是 :

根據時間線畫圖如下:

而如果是冷信号的情況的話,就有如下時間線:

可以發現,對于冷信号,類似于重播,每個訂閱者都會觀察到整個消息的處理過程.而對于​<code>​subject​</code>​,類似于直播,多個訂閱者接收到一個信号的事件,并且如果信号已經發送過的消息,錯過就無法再次接收到.

而還有一個​<code>​replaySubject​</code>​對象,将上面代碼改寫為該對象後,得到的效果圖如下:

發現這個信号會儲存之前發送過的信号,在新的對象訂閱時,将之前的信号發送.

​<code>​RACSubject​</code>​是支援​<code>​RACSubscriber​</code>​協定的,熱信号的實作就是通過這個​<code>​RACSubject​</code>​來訂閱一個冷信号,然後其他人在再來訂閱這個​<code>​RACSubject​</code>​.

觀察以下代碼:

輸出結果為 :

得到下圖:

這樣自行處理熱信号,過于簡單,有一些問題,如當​<code>​Subject​</code>​取消訂閱時,不能取消對應的冷信号的訂閱.而RAC中有對冷信号轉換為熱信号的标準接口 :

RAC提供以上五個方法中,最重要的就是 ​<code>​- (RACMulticastConnection *)multicast:(RACSubject *)subject​</code>​,其他都是基于這個實作的.看一下這個函數的實作:

50

51

52

53

54

55

56

簡單說明一下流程:

​<code>​multicast:​</code>​方法将 ​<code>​signal​</code>​和​<code>​subject​</code>​作為參數建立一個​<code>​RACMulticastConnect​</code>​的熱信号.

在​<code>​RACMulticastConnection​</code>​的 ​<code>​initWithSourceSignal: subject :​</code>​初始化時,建立一個​<code>​RACSerialDisposable​</code>​對象用于取消訂閱.

在對​<code>​RACMulticastConnection​</code>​對象調用​<code>​connect​</code>​時,會判斷熱信号是否已經與原始信号連接配接在一起了,如果沒有的話,則用​<code>​_signal​</code>​這個對象訂閱​<code>​sourceSignal​</code>​.

而這個​<code>​_signal​</code>​是一個​<code>​RACSubject​</code>​的對象,是以是一個熱信号,會在​<code>​connect​</code>​時訂閱​<code>​sourceSignal​</code>​,然後傳遞事件.

然後再來看一下 另外4個方法的實作:

對于​<code>​publish​</code>​,建立一個普通的​<code>​RACSubject​</code>​對象,一個普通的熱信号.

對于​<code>​replay​</code>​ ,建立一個​<code>​RACReplySubject​</code>​對象的熱信号,這個熱信号會重播之前的曆史信号值.

對于​<code>​replayLast​</code>​ ,以​<code>​RACReplySubject​</code>​對象建立一個熱信号,但是設定​<code>​Capacity​</code>​為1,也就是隻重發最後一次的曆史值.

對于​<code>​replayLazily​</code>​ ,使用​<code>​defer​</code>​指令,隻在信号真正被訂閱時,才去連接配接熱信号.

​<code>​RACCommand​</code>​對信号進行封裝,是信号在某些事件發生時觸發,一般與UI操作結合.​<code>​RACCommand​</code>​是對信号的一層非常漂亮的封裝,用事件來觸發信号執行的設計,使​<code>​RACCommand​</code>​适用于許多情況,不僅僅是UI操作,其内部對信号熱化,以及監控信号狀态,可以用于一些耗時操作的信号化,如網絡請求.

我們首先來看一下,​<code>​RACCommand​</code>​提供的接口:

在調用​<code>​execute:​</code>​後,一個傳回信号的信号. 這個信号是一個信号的信号,封裝了​<code>​workSignal​</code>​(本文之後用​<code>​workSignal​</code>​這個詞來形容使用者于​<code>​RACCommand​</code>​中要執行的信号).當​<code>​receiver​</code>​是​<code>​enable​</code>​的時候,發送信号.​<code>​RACCommand​</code>​将正在執行的信号封裝在這裡,作為​<code>​executionSignals​</code>​的傳回值,而信号中的​<code>​error​</code>​被發往​<code>​RACCommand​</code>​的​<code>​errors​</code>​的信号中了,而遇到error信号時,​<code>​executionSignals​</code>​的信号會傳回一個​<code>​completed​</code>​信号以标記事件完成:

如上代碼,最終輸出為:

但是,可以通過​<code>​materialize​</code>​方法來擷取這個​<code>​inner errors​</code>​.

​<code>​RACCommand​</code>​用​<code>​executionSignals​</code>​封裝​<code>​workSignal​</code>​,​<code>​workSignal​</code>​的執行必須調用​<code>​excute​</code>​來驅動.内部信号的訂閱操作會在主線程中執行.

表示目前指令是否正在執行的信号.​<code>​RACCommand​</code>​被​<code>​excute​</code>​調用後,且在信号終止前,這個信号會發送​<code>​YES​</code>​.當信号結束了,會發送​<code>​NO​</code>​.

​<code>​RACCommand​</code>​中的信号全是熱信号,可以随便訂閱,且所有訂閱結果都在主線程中執行. 這個​<code>​executing​</code>​使用的是​<code>​replayLast​</code>​,是以訂閱時就會獲得目前執行的狀态.

上面說到,當​<code>​workSignal​</code>​失敗時,也會發送一個​<code>​Completed​</code>​的事件,而這個事件也是​<code>​executing​</code>​正确處理信号狀态的前提.是以在​<code>​workSignal​</code>​一定要正确處理信号的狀态,在信号處理完成或者失敗的時候,要正确地發送​<code>​Completed​</code>​或者​<code>​error​</code>​事件.

決定 ​<code>​workSignal​</code>​是否可以執行的 信号.

隻在兩種情況下傳回 NO :

這個​<code>​RACCommand​</code>​是使用​<code>​initWithEnabled:signalBlock:​</code>​初始化,即設定了一個​<code>​enabledSignal​</code>​,而且這個信号目前傳回​<code>​NO​</code>​.

​<code>​allowsConcurrentExecution​</code>​屬性設定為 ​<code>​NO​</code>​,且這個信号正在執行中.

除了這兩種情況,一般都傳回YES.

這個信号 一般用于操作UI控件的狀态,如​<code>​UIButton​</code>​的狀态,當未滿足某些判斷邏輯時,​<code>​enabled​</code>​為NO,同時設定​<code>​UIButton​</code>​不可點選.

​<code>​workSignal​</code>​的錯誤事件被轉發到這裡.

注意,這裡錯誤訂閱需要訂閱​<code>​Next​</code>​事件,而不是​<code>​Error​</code>​事件,因為在​<code>​RACSteam​</code>​中,錯誤事件的發生會關閉信号流.

​<code>​workSignal​</code>​信号是否支援并發.

預設是​<code>​NO​</code>​,即​<code>​RACCommand​</code>​封裝的​<code>​workSignal​</code>​同時隻能有一個信号正在執行.

以一個傳回​<code>​workSignal​</code>​的​<code>​signalBlock​</code>​初始化,沒有​<code>​enableBlock​</code>​.

初始化時,設定 ​<code>​signalBlock​</code>​,和​<code>​enabledSignal​</code>​.

對于​<code>​enabledSignal​</code>​,初始狀态是​<code>​YES​</code>​, 即可用.

對于​<code>​signalBlock​</code>​,其中傳回一個​<code>​workSignal​</code>​,這個信号可以帶一個輸入值,傳回的​<code>​workSignal​</code>​會以 ​<code>​executionSignals​</code>​的值的形式被訂閱者擷取,​<code>​workSignal​</code>​會被加熱,而​<code>​executionSignals​</code>​本身也是一個熱信号.

當​<code>​RACCommand​</code>​是enable的時候,調用​<code>​execute​</code>​,

執行​<code>​signalBlock​</code>​中的初始化操作,以輸入值​<code>​input​</code>​來初始化一個新的​<code>​workSignal​</code>​

加熱​<code>​workSignal​</code>​,使用​<code>​RACReplaySubject​</code>​.

将這個熱的​<code>​workSignal​</code>​在​<code>​executionSignals​</code>​上發送.

訂閱者通過​<code>​switchToLatest​</code>​來擷取這個​<code>​workSignal​</code>​,并在主線程中訂閱事件.

這個函數是有傳回值,傳回值也是一個​<code>​RACSignal​</code>​,傳回值信号為 加熱後的​<code>​workSignal​</code>​.而如果command的enbaled信号為No時,會傳回一個 發送一個​<code>​RACCommandErrorNotEnabled​</code>​錯誤的信号.

在​<code>​RACCommand​</code>​中,封裝的所有信号都是 熱信号,訂閱事件會發生在主線程上.

我們将​<code>​workSignal​</code>​考慮為一個任務,而​<code>​RACCommand​</code>​為這個任務提供了非常友善的 狀态監控,并發控制,參數傳遞等功能. 我們可以将這樣的一個任務以信号的形式融入​<code>​RAC​</code>​的世界中,而不用自己去考慮冷熱信号,不用去考慮狀态控制.​<code>​RACCommand​</code>​是我們使用​<code>​RAC​</code>​的一個強力而友善的工具.

我們可以将很多任務通過​<code>​RACCommand​</code>​進行封裝,如網絡請求,類似下面這種封裝 :

像這樣通過​<code>​RACCommand​</code>​封裝網絡請求,通過​<code>​excute:​</code>​傳入一次網絡請求的參數,伴随着​<code>​RACCommand​</code>​完整的熱信号保證,和狀态控制,使用​<code>​ReactiveCocoa​</code>​會變得更加友善高效.

------------------越是喧嚣的世界,越需要甯靜的思考------------------

合抱之木,生于毫末;九層之台,起于壘土;千裡之行,始于足下。

積土成山,風雨興焉;積水成淵,蛟龍生焉;積善成德,而神明自得,聖心備焉。故不積跬步,無以至千裡;不積小流,無以成江海。骐骥一躍,不能十步;驽馬十駕,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓無爪牙之利,筋骨之強,上食埃土,下飲黃泉,用心一也。蟹六跪而二螯,非蛇鳝之穴無可寄托者,用心躁也。