我的Github地址 : Jerry4me, 本文章的demo連結 : JRReactiveCocoa
RAC與MVVM如今已經不是一個新鮮的玩意了, 對於介紹他們兩的精品文章更是大把, 這篇文章主要是用來記錄自己學習RAC的過程以及RAC的一些用法, 以防以後要用到的時候卻記不起來了.
具體RAC的用法以及本文出現的程式碼均能在我的 Github上, 另外附有2個MVVM的小demo. 歡迎大家檢視, 賞臉的給個star~
RAC程式設計思想
程式設計學的是思想, 學一樣東西最主要是學會它的思想, 那才是它的靈魂, 而不是學習呼叫方法而已.
RAC又被稱為FRP, 函式響應式程式設計.
何為函式式? 把操作寫成一系列巢狀的函式或者方法呼叫
1 |
[[[[Person getup] eat] run] goHome]; |
何為響應式? 不需要考慮呼叫順序, 只考慮結果. 一個屬性, 一個請求改變馬上引發一系列改變.
1 2 |
data stream -> (filter, combine, map, ...) -> another stream stream是基於時間上的事件流 |
所以RAC即糅合了函式式和響應式程式設計的優點, 使用RAC程式設計不需要考慮程式碼呼叫順序, 只需要考慮結果. 把每一個操作都寫成一系列的巢狀的方法, 使程式碼變得高內聚, 低耦合.
RAC使用場景
資料隨著時間而產生, 例如以下三點 :
- UI操作, 連續的動作和動畫部分, 例如某些控制元件跟隨滾動
- 網路庫, 因為資料是在一定時間後才返回回來, 不是立刻返回的
- 重新整理的業務邏輯, 當觸發點是多種的時候, 業務往往會變得很複雜, 用delegate, notification, observe混用, 難以統一. 這時用RAC能保證上層的高度一致性, 從而簡化邏輯上分層.
RAC類關係圖
RAC類的關係圖如下, 下面會抽出一部分類進行講解, 另外有部分類與用法會在github上的demo上看得到, 還有部分類將不在本文中出現, 本文(demo)只說明瞭一些常用的類與方法.
訊號源
RACSignal
RACSignal只會向訂閱者傳送三種事件 : next
, error
和 completed
.
RACSignal的一系列功能是通過類簇來實現的. 如 :
1 2 3 4 5 |
RACEmptySignal :空訊號,用來實現 RACSignal 的 +empty 方法; RACReturnSignal :一元訊號,用來實現 RACSignal 的 +return: 方法; RACDynamicSignal :動態訊號,使用一個 block 來實現訂閱行為,我們在使用 RACSignal 的 +createSignal: 方法時建立的就是該類的例項; RACErrorSignal :錯誤訊號,用來實現 RACSignal 的 +error: 方法; RACChannelTerminal :通道終端,代表 RACChannel 的一個終端,用來實現雙向繫結。 |
核心方法 : -subscribe:
.
RACSubject
繼承自RACSignal, 是可以手動控制的訊號, 相當於RACSignal的可變版本.
能作為訊號源被訂閱者訂閱, 又能作為訂閱者訂閱其他訊號源(實現了RACSubscriber協議).
RACSubject有三個用來實現不同功能的子類 :
1 2 3 |
RACGroupedSignal :分組訊號,用來實現 RACSignal 的分組功能; RACBehaviorSubject :重演最後值的訊號,當被訂閱時,會向訂閱者傳送它最後接收到的值; RACReplaySubject :重演訊號,儲存傳送過的值,當被訂閱時,會向訂閱者重新傳送這些值。 |
RACSequence
代表的是一個不可變的值的序列. 不能被訂閱者訂閱, 但是能與RACSignal之間非常方便地進行轉換.
RACSequence由兩部分組成 : head
和 tail
, head是序列中的第一個物件, tail則是其餘的全部物件.
RACSequence存在的最大意義就是簡化OC中的集合操作. 並且RACSequence所包含的值預設是懶計算的, 所以不知不覺中提高了我們應用的效能.
push-driven與pull-driven
- RACSignal : push-driven, 生產一個吃一個, 類似於工廠的主動生產模式, 生產出產品就push給供銷商.
- RACSequence : pull-driven, 吃一個生產一個, 類似於工廠的被動生產模式, 供銷商過來pull的時候才現做產品.
對於RACSignal的push-driven模式來說, 沒有供銷商(subscriber
)籤合同要產品, 當然就不生產了. 只有一個以上準備收貨的供銷商時, 工廠才開始生產. 這就是RACSignal的休眠(cold)和啟用(hot)狀態, 也就是冷訊號
和熱訊號
. 一般情況下RACSignal建立以後都處於cold狀態, 當有人去subscribe
才變成hot狀態.
冷訊號與熱訊號
熱訊號 : 主動, 即使你沒有訂閱事件, 仍然會時刻推送. 熱訊號可以有多個訂閱者, 是一對多的關係, 訊號可以與訂閱者共享資訊.
冷訊號 : 被動, 只有當你訂閱的時候, 它才會釋出訊息. 冷訊號只能一對一, 當有不同的訂閱者, 訊息是重新完整傳送的.
ps : 任何的訊號轉換即是對原有訊號進行訂閱從而產生新的訊號. (例如 : Map
, FlattenMap
等等)
如何區分熱訊號和冷訊號
Subject類似於直播, 錯過了就不再處理, 而Signal類似於點播, 每次訂閱都從頭開始重新傳送.
我們能得出 :
1 2 |
1. RACSubject及其子類是熱訊號 2. RACSignal排除RACSubject類以外的都是冷訊號 |
將冷訊號轉化成熱訊號
RAC幫我們封裝了一套可以輕鬆將冷訊號轉換成熱訊號的API :
1 2 3 4 5 |
- (RACMulticastConnection *)publish; - (RACMulticastConnection *)multicast:(RACSubject *)subject; - (RACSignal *)replay; - (RACSignal *)replayLast; - (RACSignal *)replayLazily; // 跟replay的區別是replayLazily會在第一次訂閱的時候才訂閱sourceSignal |
其中最重要的就是- (RACMulticastConnection *)multicast:(RACSubject *)subject;
, 其他幾個方法都是間接呼叫它的.
本質 : 使用一個Subject來訂閱原始訊號, 並讓其他訂閱者訂閱這個Subject, 由於RACSubject本身為熱訊號, 所以源訊號此時就像由冷訊號變成了熱訊號.
訂閱者
RACSubscriber
其中 -sendNext:
, -sendError:
和 -sendCompleted
分別用來從RACSignal接收 next
, error
和 completed
事件, 而-didSubscribeWithDisposable:
則用來接收代表某次訂閱的disposable物件.
一個RACDisposable物件就代表這一次訂閱, 並且我們可以用它來取消這次訂閱.
RACSubscriber就是真正的訂閱者, 而RACPassthroughSubscriber可以使得一個訂閱者可以訂閱多個訊號源, 即擁有多個RACDisposable物件, 並能隨時取消其中的任何一次訂閱. 為了實現這個功能, RAC就引入了RACPassthroughSubscriber類, 它是RACSubscriber類的一個裝飾器, 封裝了一個真正的訂閱者 RACSubscriber 物件, 它負責轉發所有事件給這個真正的訂閱者, 而當此次訂閱被取消時, 它就會停止轉發
RACMulticastConnection
使得不管外面有多少個訂閱者, 對源訊號的訂閱只會有一次. 為了防止副作用的產生, 使用的便是multicast機制
multicast的機制
機制一 : 能防止某訊號被多次訂閱時呼叫多次didSubscribe block產生副作用.
機制二 : 實現replay, 即每當有訂閱者訂閱時, 會將之前快取中的sendNext重新傳送給該訂閱者.
副作用
- 函式的處理過程中, 修改了外部的變數(例如 : 全域性變數, 成員變數等)
- 函式的處理過程中, 出發了一些額外的動作(例如 : 傳送了一個全域性的Notification, 在console列印了一行資訊, 儲存了檔案, 觸發了網路, 更新了螢幕等)
- 函式的處理過程中, 受到外部變數的影響(例如 : 全域性變數, 成員變數等, block中捕獲到的外部變數也算)
- 函式的處理過程中, 受到執行緒鎖的影響
以上都算副作用. 然而冷訊號有可能因為有多個訂閱者訂閱而產生極大的副作用, 例如傳送了同一個網路請求若干次, 同一個計算做了若干次等等, 這些問題都可以通過把這個冷訊號轉化成熱訊號得以解決.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id subscriber) { NSLog(@"建立"); /* 傳送網路請求 */ [subscriber sendNext:@"data"]; [subscriber sendCompleted]; return [RACDisposable disposableWithBlock:^{ NSLog(@"銷燬"); }]; }]; [signal subscribeNext:^(id x) { // 第一個訂閱者 NSLog(@"id = %@", x); }]; [signal subscribeNext:^(id x) { // 第二個訂閱者 NSLog(@"id2 = %@", x); }]; /* 控制檯輸出為 : 2017-03-13 15:48:09.632 使用cocoapods[41347:10397774] 建立 2017-03-13 15:48:09.634 使用cocoapods[41347:10397774] id = data 2017-03-13 15:48:09.636 使用cocoapods[41347:10397774] 銷燬 2017-03-13 15:48:09.637 使用cocoapods[41347:10397774] 建立 2017-03-13 15:48:09.638 使用cocoapods[41347:10397774] id2 = data 2017-03-13 15:48:09.639 使用cocoapods[41347:10397774] 銷燬 由此可見有多個訂閱者訂閱了該訊號源的話, 就會多次呼叫訊號源block中的方法, 產生副作用 */ |
排程器
RACScheduler
RAC中對GCD的簡單封裝. 子類如下 :
1 2 3 4 |
RACImmediateScheduler :立即執行排程的任務,這是唯一一個支援同步執行的排程器; RACQueueScheduler :一個抽象的佇列排程器,在一個 GCD 序列列隊中非同步排程所有任務; RACTargetQueueScheduler :繼承自 RACQueueScheduler ,在一個以一個任意的 GCD 佇列為 target 的序列佇列中非同步排程所有任務; RACSubscriptionScheduler :一個只用來排程訂閱的排程器。 |
清潔工
RACDisposable
在訂閱者訂閱訊號源的過程中, 可能會產生副作用或者消耗一定的資源, 所以在取消訂閱或完成訂閱的時候我們就需要做一些資源回收和辣雞清理的工作. 核心方法為-dispose
1 2 3 4 |
RACSerialDisposable :作為 disposable 的容器使用,可以包含一個 disposable 物件,並且允許將這個 disposable 物件通過原子操作交換出來; RACKVOTrampoline :代表一次 KVO 觀察,並且可以用來停止觀察; RACCompoundDisposable :跟 RACSerialDisposable 一樣,RACCompoundDisposable 也是作為 disposable 的容器使用。不同的是,它可以包含多個 disposable 物件,並且支援手動新增和移除 disposable 物件,有點類似於可變陣列 NSMutableArray 。而當一個 RACCompoundDisposable 物件被 disposed 時,它會呼叫其所包含的所有 disposable 物件的 -dispose 方法,有點類似於 autoreleasepool 的作用; RACScopedDisposable :當它被 dealloc 的時候呼叫本身的 -dispose 方法。 |
總的來說就是在適當的時機呼叫disposable物件的-dispose
方法而已.
RAC常見巨集
用法在demo中
1 2 3 4 5 6 7 8 9 |
1. RAC(TARGET, [KEYPATH, [NIL_VALUE]]) -> 總是出現在等號左邊, 等號右邊是一個RACSignal 2. RACObserve(TARGET, KEYPATH) -> 產生一個RACSignal 3. @weakify(self) 和 @strongify(self) 4. RACTuplePack 和 RACTupleUnpack -> 壓包與解包 5. @keypath(self.property) -> 產生一個字串@"property" |
RAC中潛在的記憶體洩漏及解決方法
RACObserve
如果在block中使用到了RACObserve, 則必須加上@weakify
和@strongify
, 儘管沒有顯示使用到了self
. 文件事例如下 :
1 2 3 4 5 6 |
@weakify(self); RACSignal *signal3 = [anotherSignal flattenMap:^(NSArrayController *arrayController) { // Avoids a retain cycle because of RACObserve implicitly referencing self @strongify(self); return RACObserve(arrayController, items); }]; |
RACSubject
RACSubject例項進行map
操作之後, 傳送完畢一定要呼叫-sendCompleted
, 否則會出現記憶體洩漏; 而RACSignal例項不管是否進行map
操作, 不管是否呼叫-sendCompleted
, 都不會出現記憶體洩漏.
原因 : 因為RACSubject是熱訊號, 為了保證未來有事件發生的時候, 訂閱者可以收到資訊, 所以需要對持有訂閱者!
ps : 幾乎所有操作底層都會呼叫bind
這樣一個方法, 包括但不限於以下方法 : map
, filter
, merge
, combineLatest
, flattenMap
…
1 2 3 4 |
map : map -> flattenMap -> bind filter : filter -> flattenMap -> bind |
所以 : 對訊號操作完成記得傳送-sendCompleted
. (或者-sendError
).
執行緒安全
Signal events是線性的, 不會出現併發的情況, 除非顯示地指定Scheduler. 所以-subscribeNext:
裡的block不需要加鎖, 其他的events會依次排隊, 直到block處理完成.
為了方便除錯, 最好給訊號指定Name : -setNameWithFormat:
參考文章 :
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式