我的Github地址 : Jerry4me, 本文章的demo鏈接 : JRReactiveCocoa
RAC與MVVM如今已經(jīng)不是一個新鮮的玩意了, 對于介紹他們兩的精品文章更是大把, 這篇文章主要是用來記錄自己學(xué)習(xí)RAC的過程以及RAC的一些用法, 以防以后要用到的時候卻記不起來了.
具體RAC的用法以及本文出現(xiàn)的代碼均能在我的 Github上, 另外附有2個MVVM的小demo. 歡迎大家查看, 賞臉的給個star~
RAC編程思想
編程學(xué)的是思想, 學(xué)一樣?xùn)|西最主要是學(xué)會它的思想, 那才是它的靈魂, 而不是學(xué)習(xí)調(diào)用方法而已.
RAC又被稱為FRP, 函數(shù)響應(yīng)式編程.
何為函數(shù)式? 把操作寫成一系列嵌套的函數(shù)或者方法調(diào)用
[[[[Person getup] eat] run] goHome];
何為響應(yīng)式? 不需要考慮調(diào)用順序, 只考慮結(jié)果. 一個屬性, 一個請求改變馬上引發(fā)一系列改變.
data stream -> (filter, combine, map, ...) -> another stream
stream是基于時間上的事件流
所以RAC即糅合了函數(shù)式和響應(yīng)式編程的優(yōu)點, 使用RAC編程不需要考慮代碼調(diào)用順序, 只需要考慮結(jié)果. 把每一個操作都寫成一系列的嵌套的方法, 使代碼變得高內(nèi)聚, 低耦合.
RAC使用場景
數(shù)據(jù)隨著時間而產(chǎn)生, 例如以下三點 :
- UI操作, 連續(xù)的動作和動畫部分, 例如某些控件跟隨滾動
- 網(wǎng)絡(luò)庫, 因為數(shù)據(jù)是在一定時間后才返回回來, 不是立刻返回的
- 刷新的業(yè)務(wù)邏輯, 當(dāng)觸發(fā)點是多種的時候, 業(yè)務(wù)往往會變得很復(fù)雜, 用delegate, notification, observe混用, 難以統(tǒng)一. 這時用RAC能保證上層的高度一致性, 從而簡化邏輯上分層.
RAC類關(guān)系圖
RAC類的關(guān)系圖如下, 下面會抽出一部分類進(jìn)行講解, 另外有部分類與用法會在github上的demo上看得到, 還有部分類將不在本文中出現(xiàn), 本文(demo)只說明了一些常用的類與方法.
信號源
RACSignal
RACSignal只會向訂閱者發(fā)送三種事件 : next
, error
和 completed
.
RACSignal的一系列功能是通過類簇來實現(xiàn)的. 如 :
RACEmptySignal :空信號,用來實現(xiàn) RACSignal 的 +empty 方法;
RACReturnSignal :一元信號,用來實現(xiàn) RACSignal 的 +return: 方法;
RACDynamicSignal :動態(tài)信號,使用一個 block 來實現(xiàn)訂閱行為,我們在使用 RACSignal 的 +createSignal: 方法時創(chuàng)建的就是該類的實例;
RACErrorSignal :錯誤信號,用來實現(xiàn) RACSignal 的 +error: 方法;
RACChannelTerminal :通道終端,代表 RACChannel 的一個終端,用來實現(xiàn)雙向綁定。
核心方法 : -subscribe:
.
RACSubject
繼承自RACSignal, 是可以手動控制的信號, 相當(dāng)于RACSignal的可變版本.
能作為信號源被訂閱者訂閱, 又能作為訂閱者訂閱其他信號源(實現(xiàn)了RACSubscriber協(xié)議).
RACSubject有三個用來實現(xiàn)不同功能的子類 :
RACGroupedSignal :分組信號,用來實現(xiàn) RACSignal 的分組功能;
RACBehaviorSubject :重演最后值的信號,當(dāng)被訂閱時,會向訂閱者發(fā)送它最后接收到的值;
RACReplaySubject :重演信號,保存發(fā)送過的值,當(dāng)被訂閱時,會向訂閱者重新發(fā)送這些值。
RACSequence
代表的是一個不可變的值的序列. 不能被訂閱者訂閱, 但是能與RACSignal之間非常方便地進(jìn)行轉(zhuǎn)換.
RACSequence由兩部分組成 : head
和 tail
, head是序列中的第一個對象, tail則是其余的全部對象.
RACSequence存在的最大意義就是簡化OC中的集合操作. 并且RACSequence所包含的值默認(rèn)是懶計算的, 所以不知不覺中提高了我們應(yīng)用的性能.
push-driven與pull-driven
RACSignal : push-driven, 生產(chǎn)一個吃一個, 類似于工廠的主動生產(chǎn)模式, 生產(chǎn)出產(chǎn)品就push給供銷商.
RACSequence : pull-driven, 吃一個生產(chǎn)一個, 類似于工廠的被動生產(chǎn)模式, 供銷商過來pull的時候才現(xiàn)做產(chǎn)品.
對于RACSignal的push-driven模式來說, 沒有供銷商(subscriber
)簽合同要產(chǎn)品, 當(dāng)然就不生產(chǎn)了. 只有一個以上準(zhǔn)備收貨的供銷商時, 工廠才開始生產(chǎn). 這就是RACSignal的休眠(cold)和激活(hot)狀態(tài), 也就是冷信號
和熱信號
. 一般情況下RACSignal創(chuàng)建以后都處于cold狀態(tài), 當(dāng)有人去subscribe
才變成hot狀態(tài).
冷信號與熱信號
熱信號 : 主動, 即使你沒有訂閱事件, 仍然會時刻推送. 熱信號可以有多個訂閱者, 是一對多的關(guān)系, 信號可以與訂閱者共享信息.
冷信號 : 被動, 只有當(dāng)你訂閱的時候, 它才會發(fā)布消息. 冷信號只能一對一, 當(dāng)有不同的訂閱者, 消息是重新完整發(fā)送的.
ps : 任何的信號轉(zhuǎn)換即是對原有信號進(jìn)行訂閱從而產(chǎn)生新的信號. (例如 : Map
, FlattenMap
等等)
如何區(qū)分熱信號和冷信號
Subject類似于直播, 錯過了就不再處理, 而Signal類似于點播, 每次訂閱都從頭開始重新發(fā)送.
我們能得出 :
- RACSubject及其子類是熱信號
- RACSignal排除RACSubject類以外的都是冷信號
將冷信號轉(zhuǎn)化成熱信號
RAC幫我們封裝了一套可以輕松將冷信號轉(zhuǎn)換成熱信號的API :
- (RACMulticastConnection *)publish;
- (RACMulticastConnection *)multicast:(RACSubject *)subject;
- (RACSignal *)replay;
- (RACSignal *)replayLast;
- (RACSignal *)replayLazily; // 跟replay的區(qū)別是replayLazily會在第一次訂閱的時候才訂閱sourceSignal
其中最重要的就是- (RACMulticastConnection *)multicast:(RACSubject *)subject;
, 其他幾個方法都是間接調(diào)用它的.
本質(zhì) : 使用一個Subject來訂閱原始信號, 并讓其他訂閱者訂閱這個Subject, 由于RACSubject本身為熱信號, 所以源信號此時就像由冷信號變成了熱信號.
訂閱者
RACSubscriber
其中 -sendNext:
, -sendError:
和 -sendCompleted
分別用來從RACSignal接收 next
, error
和 completed
事件, 而-didSubscribeWithDisposable:
則用來接收代表某次訂閱的disposable對象.
一個RACDisposable對象就代表這一次訂閱, 并且我們可以用它來取消這次訂閱.
RACSubscriber就是真正的訂閱者, 而RACPassthroughSubscriber可以使得一個訂閱者可以訂閱多個信號源, 即擁有多個RACDisposable對象, 并能隨時取消其中的任何一次訂閱. 為了實現(xiàn)這個功能, RAC就引入了RACPassthroughSubscriber類, 它是RACSubscriber類的一個裝飾器, 封裝了一個真正的訂閱者 RACSubscriber 對象, 它負(fù)責(zé)轉(zhuǎn)發(fā)所有事件給這個真正的訂閱者, 而當(dāng)此次訂閱被取消時, 它就會停止轉(zhuǎn)發(fā)
RACMulticastConnection
使得不管外面有多少個訂閱者, 對源信號的訂閱只會有一次. 為了防止副作用的產(chǎn)生, 使用的便是multicast機制
multicast的機制
機制一 : 能防止某信號被多次訂閱時調(diào)用多次didSubscribe block產(chǎn)生副作用.
機制二 : 實現(xiàn)replay, 即每當(dāng)有訂閱者訂閱時, 會將之前緩存中的sendNext重新發(fā)送給該訂閱者.
副作用
- 函數(shù)的處理過程中, 修改了外部的變量(例如 : 全局變量, 成員變量等)
- 函數(shù)的處理過程中, 出發(fā)了一些額外的動作(例如 : 發(fā)送了一個全局的Notification, 在console打印了一行信息, 保存了文件, 觸發(fā)了網(wǎng)絡(luò), 更新了屏幕等)
- 函數(shù)的處理過程中, 受到外部變量的影響(例如 : 全局變量, 成員變量等, block中捕獲到的外部變量也算)
- 函數(shù)的處理過程中, 受到線程鎖的影響
以上都算副作用. 然而冷信號有可能因為有多個訂閱者訂閱而產(chǎn)生極大的副作用, 例如發(fā)送了同一個網(wǎng)絡(luò)請求若干次, 同一個計算做了若干次等等, 這些問題都可以通過把這個冷信號轉(zhuǎn)化成熱信號得以解決.
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"創(chuàng)建");
/* 發(fā)送網(wǎng)絡(luò)請求 */
[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] 創(chuàng)建
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] 創(chuàng)建
2017-03-13 15:48:09.638 使用cocoapods[41347:10397774] id2 = data
2017-03-13 15:48:09.639 使用cocoapods[41347:10397774] 銷毀
由此可見有多個訂閱者訂閱了該信號源的話, 就會多次調(diào)用信號源block中的方法, 產(chǎn)生副作用
*/
調(diào)度器
RACScheduler
RAC中對GCD的簡單封裝. 子類如下 :
RACImmediateScheduler :立即執(zhí)行調(diào)度的任務(wù),這是唯一一個支持同步執(zhí)行的調(diào)度器;
RACQueueScheduler :一個抽象的隊列調(diào)度器,在一個 GCD 串行列隊中異步調(diào)度所有任務(wù);
RACTargetQueueScheduler :繼承自 RACQueueScheduler ,在一個以一個任意的 GCD 隊列為 target 的串行隊列中異步調(diào)度所有任務(wù);
RACSubscriptionScheduler :一個只用來調(diào)度訂閱的調(diào)度器。
清潔工
RACDisposable
在訂閱者訂閱信號源的過程中, 可能會產(chǎn)生副作用或者消耗一定的資源, 所以在取消訂閱或完成訂閱的時候我們就需要做一些資源回收和辣雞清理的工作. 核心方法為-dispose
RACSerialDisposable :作為 disposable 的容器使用,可以包含一個 disposable 對象,并且允許將這個 disposable 對象通過原子操作交換出來;
RACKVOTrampoline :代表一次 KVO 觀察,并且可以用來停止觀察;
RACCompoundDisposable :跟 RACSerialDisposable 一樣,RACCompoundDisposable 也是作為 disposable 的容器使用。不同的是,它可以包含多個 disposable 對象,并且支持手動添加和移除 disposable 對象,有點類似于可變數(shù)組 NSMutableArray 。而當(dāng)一個 RACCompoundDisposable 對象被 disposed 時,它會調(diào)用其所包含的所有 disposable 對象的 -dispose 方法,有點類似于 autoreleasepool 的作用;
RACScopedDisposable :當(dāng)它被 dealloc 的時候調(diào)用本身的 -dispose 方法。
總的來說就是在適當(dāng)?shù)臅r機調(diào)用disposable對象的-dispose
方法而已.
RAC常見宏
用法在demo中
1. RAC(TARGET, [KEYPATH, [NIL_VALUE]]) -> 總是出現(xiàn)在等號左邊, 等號右邊是一個RACSignal
2. RACObserve(TARGET, KEYPATH) -> 產(chǎn)生一個RACSignal
3. @weakify(self) 和 @strongify(self)
4. RACTuplePack 和 RACTupleUnpack -> 壓包與解包
5. @keypath(self.property) -> 產(chǎn)生一個字符串@"property"
RAC中潛在的內(nèi)存泄漏及解決方法
RACObserve
如果在block中使用到了RACObserve, 則必須加上@weakify
和@strongify
, 盡管沒有顯示使用到了self
. 文檔事例如下 :
@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實例進(jìn)行map
操作之后, 發(fā)送完畢一定要調(diào)用-sendCompleted
, 否則會出現(xiàn)內(nèi)存泄漏; 而RACSignal實例不管是否進(jìn)行map
操作, 不管是否調(diào)用-sendCompleted
, 都不會出現(xiàn)內(nèi)存泄漏.
原因 : 因為RACSubject是熱信號, 為了保證未來有事件發(fā)生的時候, 訂閱者可以收到信息, 所以需要對持有訂閱者!
ps : 幾乎所有操作底層都會調(diào)用bind
這樣一個方法, 包括但不限于以下方法 : map
, filter
, merge
, combineLatest
, flattenMap
...
map :
map -> flattenMap -> bind
filter :
filter -> flattenMap -> bind
所以 : 對信號操作完成記得發(fā)送-sendCompleted
. (或者-sendError
).
線程安全
Signal events是線性的, 不會出現(xiàn)并發(fā)的情況, 除非顯示地指定Scheduler. 所以-subscribeNext:
里的block不需要加鎖, 其他的events會依次排隊, 直到block處理完成.
為了方便調(diào)試, 最好給信號指定Name : -setNameWithFormat:
參考文章 :