本文簡單介紹了ReactiveCocoa的基礎用法,希望讀完能對這個框架的使用有一個大概的了解。
前面幾篇文章,我們研究了ReactiveCocoa(以下簡稱RAC)的起源和思想。網絡上介紹RAC的使用的好文甚多,在文末的Reference中標出了一些以供參考。RAC這個開源庫本身也是十分良心,代碼注釋十分齊全,因此這一篇文章不做太多的擴展,僅談一談RAC的基礎使用方法。
RACSignal
上文中說到,函數響應式編程中,將各種通信機制所需要解決的「輸入」與「輸出」的異步關系抽象成了事件/時間驅動的值流,并通過monad
使其支持了函數式編程的特性。而在RAC中,這個東西就是RACStream
,開發過程中我們并不是直接使用它,而是其子類——RACSignal
和RACSequence
。這一節,講講RACSignal
。
Signal,顧名思義,代表一個信號,可以源源不斷地給你傳遞信息。這樣就好理解RACSignal
代表著「隨時間變化的值流」,這里的值,就包含了將來即將到來的「輸入」。打個比方,一個微博博主便是一個「Signal」,只要沒被封號,你就會知道將來他會一直發出消息。如果關注了這個博主,一旦他開始發消息,新消息會被自動推送到你的設備,因此說RACSignal
是一個Push-Driven
的值流。
那么,RACSignal
博主會發出什么消息呢?一個RACSignal
傳遞的值分為三類:
- Next。「Next」代表著一個新的值,一條新的微博。只要這個博主是活躍的,他就會源源不斷地發微博。
- Error。「Error」則代表著這個Signal出了什么問題,發出了一個代表「錯誤」的信號。發送出「Error」也就意味著這個Signal的消息到此為止了。比如這位博主被封號了,他就會給你發一條微博,上面寫著「404Error」,你就知道他再也不會發微博了……
- Completed。代表一個Signal完成了自己的全部信息發送。比如某天這個博主想退出微博了,于是發出最后一條微博——「ByeBye粉絲們」。這就是「Completed」。
一個Signal的信息流,都是由若干個「Next」,加上一個代表終結的「Error」或「Completed」組成的。
這些值都是從哪里來的呢?一個Signal所發出的信息主要來源有兩種:
- 手動創建一個信號時定義它發出的信息。這就好像是一位原創博主,每條微博都是他自己寫的:
// 代碼1
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"blog1"];
[subscriber sendNext:@"blog2"];
[subscriber sendNext:@"blog3"];
[subscriber sendCompleted];
return nil;
}];
- 由其他通信機制生成一個信號時,在其他通信機制產生輸入時發出消息。比如某些大V博主的微博就是專門從雜志、知乎等其他信息載體上將信息搬運過來。RAC提供了很多有力的工具,讓我們從傳統的Cocoa通信機制中制造出一個信號來:
// 代碼2
// signal from KVO
RACSignal *blogSignalA = RACObserve(someNewspaper, news);
// signal from UIControl events
RACSignal *blogSignalB = [someButton rac_signalForControlEvents:UIControlEventTouchUpInside];
// signal from selectors
RACSignal *blogSignalB = [self rac_signalForSelector:@selector(viewWillAppear:)];
好了,現在有一個博主能夠發出很多消息。但如果沒有人關注他,這些信息也不會有多大的作用。對于一個Signal也是一樣,創建不是目的,獲取它發出的信息才是我們所需要的。在RAC中,這種行為叫「訂閱」(Subscribe)。例如,我們想在收到消息時,把消息打印出來,或者做一些其他的事情:
// 代碼3
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"blog1"];
[subscriber sendNext:@"blog2"];
[subscriber sendNext:@"blog3"];
[subscriber sendCompleted];
return nil;
}];
//subscribe to blogSignalA
[blogSignalA subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
//do something else
} error:^(NSError * _Nullable error) {
NSLog(@"%@",error);
} completed:^{
NSLog(@"Complete!");
}];
現在,我們就關注了blogSignalA
這位博主,他發出的blog1
,blog2
等等微博都會推送到我們,由我們進行處理。RAC對于信號的「訂閱者」是有要求的,它必須實現了RACSubscriber
協議:
// 代碼4
@protocol RACSubscriber <NSObject>
@required
- (void)sendNext:(id)value;
- (void)sendError:(NSError *)error;
- (void)sendCompleted;
- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
@end
這也很好理解,因為「訂閱者」至少得知道自己需要用這些訂閱的值來做什么。上面的代碼3
中的subscribeNext:error:completed:
其實就是幫我們創建了一個內部的「訂閱者」,這些在后續如果深入探究源碼的時候會詳細說明。
此外,Signal支持各種函數式的操作,例如map
,reduce
,filter
等等。這可以讓我們方便地對原始信號傳輸出的信息進行一步步加工,最終得到我們所需要的值,這就是「函數性」賦予的利器:
// 代碼5
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"Sunday"];
[subscriber sendNext:@"Monday"];
[subscriber sendNext:@"Tuesday"];
[subscriber sendNext:@"Wednesday"];
[subscriber sendNext:@"Thursday"];
[subscriber sendNext:@"Friday"];
[subscriber sendNext:@"Saturday"];
[subscriber sendCompleted];
return nil;
}];
RACSignal *blogSignalB =
[blogSignalA map:^id _Nullable(NSString * _Nullable value) {
if ([value isEqualToString:@"Sunday"] || [value isEqualToString:@"Saturday"]) {
return @"Weekend";
}else {
return @"Workday";
}
}];
RACSignal *blogSignalC =
[blogSignalB filter:^BOOL(NSString * _Nullable value) {
return [value isEqualToString:@"Weekend"];
}];
[blogSignalC subscribeNext:^(id _Nullable x) {
NSLog(@"Wow Weekend! Time to Relax!");
}];
這里博主A是一個報時的微博,而博主B是一個翻譯的微博,它將A發出的微博進行加工,然后發出「Weekend」和「Workday」兩種微博。博主C負責過濾B發出的微博,屏蔽了所有工作日的消息(Nice)。最后我們關注博主C,就能在收到消息推送的時候知道該出去玩啦!當然,有了Monad
的保證,我們也可以采用鏈式語法這么寫:
// 代碼6
RACSignal *blogSignalD =
[[[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"Sunday"];
[subscriber sendNext:@"Monday"];
[subscriber sendNext:@"Tuesday"];
[subscriber sendNext:@"Wednesday"];
[subscriber sendNext:@"Thursday"];
[subscriber sendNext:@"Friday"];
[subscriber sendNext:@"Saturday"];
[subscriber sendCompleted];
return nil;
}] map:^id _Nullable(NSString * _Nullable value) {
if ([value isEqualToString:@"Sunday"] || [value isEqualToString:@"Saturday"]) {
return @"Weekend";
}else {
return @"Workday";
}
}] filter:^BOOL(id _Nullable value) {
return [value isEqualToString:@"Weekend"];
}];
[blogSignalD subscribeNext:^(id _Nullable x) {
NSLog(@"Wow Weekend! Time to Relax!");
}];
總結一下,RACSignal
的基本操作主要是三個:創建(手動創建+由其他通信機制生成),訂閱,以及轉換。
RACSubject
上面討論的RACSignal
,其實細究起其行為,是和微博不太一樣的。從代碼1
和代碼3
中可以看出,RACSignal
所能發出的信號是定義好的,即創建該Signal
的時候就已經確定了。這更像是一個「微博機器人」,每當有一個新的粉絲來訂閱它,它便按照一個「創建腳本程序」從頭開始生成若干微博進行推送。這種行為是依賴于「訂閱」的,只有當「訂閱」發生的時候,才會對新的訂閱者發送內容。我們稱之為「冷信號(Cold Signal)」。
這種「Cold Signal」會帶來一個問題。譬如說,這個機器人在它的「創建腳本程序」中進行了其他的操作:
// 代碼7
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSLog(@"Ready To Send!");
[subscriber sendNext:@"blog1"];
[subscriber sendNext:@"blog2"];
[subscriber sendNext:@"blog3"];
NSLog(@"Sending Comleted!");
[subscriber sendCompleted];
return nil;
}];
那么,每當有一個新的訂閱者,這兩個NSLog
的操作就會重復執行一遍。我們把這種操作稱為「副作用(Side-Effect)」。想象一下如果把上面簡單的NSLog
換成非常復雜的操作,比如網絡請求,那么這樣的「副作用」就非常明顯了。因為我們可能只是想進行一次網絡請求。RAC中主要使用RACSubject
來解決這個問題。
RACSubject
是RACSignal
的子類。是不同于只會根據腳本發送固定信號的RACSignal
,RACSubject
能夠由我們程序控制,在任何時候主動發送新的值。這有點類似于不可變數組和可變數組的概念。可以想象這樣一種情景,我們要在原有的舊代碼里利用RAC完成一些功能,那么可以利用RACSubject
,在老代碼中間手動控制其發送出信號。因此,官方稱RACSubject
為「most helpful in bridging the non-RAC world to RAC」。
RACSubject
是一個「熱信號」,它在內部維護了一個「訂閱者」的統計數組。每當產生新的訂閱行為的時候,它只是簡單地將這個「訂閱者」添加進自己維護的數組中。等到發出信號的時候,會遍歷該數組,向其中所有的「訂閱者」發送該信號。也就是說,它不會管有沒有訂閱行為,而只是自己發自己的信號。而訂閱之后,也只能收到它以后發出的信號。嗯,這樣的行為才是一個活生生的大V博主,而不是冰冷的腳本嘛!
同時,RACSubject
還是一個「訂閱者」,它實現了RACSubscriber
協議,也就是說,它可以訂閱一個「RACSignal」。當接受到「RACSignal」發送的信號的時候,它會遍歷其內部的「訂閱者」數組,將自己接收到的信號轉發給每一個「訂閱者」。也就是說,RACSubject
充當了一個中間轉發者的角色。這樣既保證了對原始信號只訂閱一次,從而可以消除副作用的影響,又保證了外界多個訂閱者的正常行為。
<div align=center>
</div>
在RAC中,這種關系是由RACMulticastConnection
類以及multicast
和connect
操作實現的:
// 代碼8
RACSignal *sideEffectSignal =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSLog(@"This is side-effect~~~");
[subscriber sendNext:@"1"];
[subscriber sendNext:@"2"];
[subscriber sendNext:@"3"];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
}];
}];
RACSubject *subject = [RACSubject subject];
RACMulticastConnection *multiConnect = [sideEffectSignal multicast:subject];
//subscribe A
[multiConnect.signal subscribeNext:^(id _Nullable x) {
NSLog(@"A receive next: %@",x);
} error:^(NSError * _Nullable error) {
NSLog(@"A receive error: %@",error);
} completed:^{
NSLog(@"A receive completed");
}];
//subscribe B
[multiConnect.signal subscribeNext:^(id _Nullable x) {
NSLog(@"B receive next: %@",x);
} error:^(NSError * _Nullable error) {
NSLog(@"B receive error: %@",error);
} completed:^{
NSLog(@"B receive completed");
}];
[multiConnect connect];
/*
Log result:
This is side-effect~~~
A receive next: 1
B receive next: 1
A receive next: 2
B receive next: 2
A receive next: 3
B receive next: 3
A receive completed
B receive completed
*/
RACMulticastConnection
類封裝了原始信號以及充當「中間人」的RACSubject
對象,調用[multiConnect connect]
會將「中間人」與原始信號連接起來(使用封裝的RACSubject
訂閱原始RACSignal
)。注意這里調用[multiConnect connect]
的時機,如果將其提前到subscribe A
和subscribe B
之前,那么A和B將完全接收不到原始信號發出的消息,這還是因為RACSubject
是一個「熱信號」的原因。如果確實需要先執行connect
操作,那么在創建RACMulticastConnection
時可以使用RACSubject
的子類,如RACReplaySubject
等來實現具體的需求。
RACSequence
上面說的RACSignal
是一個Push-driven
的值流,而RACSequence
則是一個Pull-driven
的值流,它們的關系就好像是后臺推送和客戶端主動拉取兩種不同行為。
RACSequence
主要用于簡化集合的操作,以及對Cocoa中的基礎集合類型提供函數性的工具。譬如說,
// 代碼9
NSArray *names = @[@"Peter",@"John",@"Steve",@"Jim"];
[[names.rac_sequence.signal filter:^BOOL(NSString * _Nullable value) {
return value.length > 4;
}] subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
}];
一般情況下,RACSequence
會采用惰性計算,即要獲取其中某個元素的時候再去對該元素進行計算。具體思想可以參考臧老師的聊一聊iOS開發中的惰性計算:
RACCommand
除了上面討論的幾種信號,RAC還為我們提供了很多實用而充滿技巧的工具類。RACCommand
就是其中一個。顧名思義,它是對一個「操作命令」的封裝:這個操作命令會產生一系列的結果輸出,而RACCommand
提供了豐富的接口來控制該操作命令的執行、取消,操作的狀態流等等。
想象一下「人民日報」微博的小編,他掌握著一份程序,能夠從人民日報官網拉取當天最新的新聞,將這些新聞生成一系列的微博發出。有了這個程序,他每天的工作就很輕松了:執行一下這個程序就可以了(小編不要打我…)
RACCommand
提供了兩個初始化方法:
- (instancetype)initWithSignalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;
- (instancetype)initWithEnabled:(nullable RACSignal<NSNumber *> *)enabledSignal signalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;
其中signalBlock
就是上面說的「會產生一系列結果輸出的操作命令」,而enabledSignal
的值則控制了是否能執行該操作。RACCommand
提供了excute
接口來執行操作命令:
- (RACSignal<ValueType> *)execute:(nullable InputType)input;
成功執行操作后,產生的結果由executionSignals
返回,每次成功執行,都會返回一個RACSignal
,所以該屬性是一個「高階信號」,即「Signal of signal」;倘若執行失敗,則會由errors
返回:
@property (nonatomic, strong, readonly) RACSignal<RACSignal<ValueType> *> *executionSignals;
@property (nonatomic, strong, readonly) RACSignal<NSError *> *errors;
RACCommand
還提供了監控當前操作狀態的屬性:
@property (nonatomic, strong, readonly) RACSignal<NSNumber *> *executing;
@property (nonatomic, strong, readonly) RACSignal<NSNumber *> *enabled;
舉個栗子:
// 代碼10
RACSignal *enalbeSignal =
[[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"Sunday"];
[subscriber sendNext:@"Monday"];
[subscriber sendNext:@"Tuesday"];
[subscriber sendNext:@"Wednesday"];
[subscriber sendNext:@"Thursday"];
[subscriber sendNext:@"Friday"];
[subscriber sendNext:@"Saturday"];
[subscriber sendCompleted];
return nil;
}] map:^id _Nullable(NSString * _Nullable value) {
if ([value isEqualToString:@"Sunday"] || [value isEqualToString:@"Saturday"]) {
return @(NO));
}else {
return @(YES));
}
}];
self.command =
[[RACCommand alloc] initWithEnabled:enalbeSignal
signalBlock:^RACSignal * _Nonnull(id _Nullable input) {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[self fetchNewsWithCallback:^(NSError *error, id result) {
if (result) {
[subscriber sendNext:result];
[subscriber sendCompleted];
}
else {
[subscriber sendError:error];
}
}];
return [RACDisposable disposableWithBlock:^{
NSLog(@"send command disposable");
}];
}] ;
}];
// The final signal for blog
RACSignal *blogSignal = [self.command.executionSignals flatten];
上面的例子創建了一個RACCommand
,用來幫助小編同學在工作日從服務器拉取新聞,然后發送微博。需要注意的是,RACCommand
在執行操作后,已將該操作生成的RACSignal
用RACReplaySubject
進行了multicast
,所以不用擔心內部操作所包含的副作用問題。
可以看出,RACCommand
和UIButton
的作用是很相似的:都是用于執行某個操作。事實上,RAC為UIButton
提供了十分方便的category,能生成一個RACCommand
并綁定到button上,使得該button的點擊事件、enable狀態等等都可以通過這個RACCommand
完成:
self.senderButton.rac_command = self.command;
總結
這里我們用微博的例子來簡單介紹了一下RAC中一些基礎組件的用法。RAC的功能遠遠不止這幾個基礎組件,甚至遠遠不止是組件所提供的api。它更代表一種編程風格,一種代碼思想。
當然,從這篇文章,以及自己的實踐也可以看出,RAC還是存在一些缺點:
- RAC對代碼的侵入性很強,如果選擇了使用它,項目代碼將和RAC庫產生很強的耦合。
- RAC不利于團隊協作。如果有些團隊成員不熟悉,那么將很難調試和修改其他成員用RAC編寫的代碼。
- 調試不友好。由于RAC內部操作相當復雜,即使一行簡單的代碼,調試時的堆棧也完全是RAC內部的堆棧信息。也因此,RAC官方更推薦使用
name
或log
進行調試。 - 學習曲線還是比較陡峭的,需要理解函數響應式編程的思想,以及學習RAC的基本知識。調bug的時候甚至還需要充分了解RAC內部原理。
因此,是否使用RAC到實際的大型項目中,這是個見仁見智的話題。但是一些小項目或是自己學習、研究、使用,還是十分有價值的。
Reference
霜神解讀RAC源碼系列
美團點評技術博客的RAC系列
Draveness解讀的RAC源碼系列
ReactiveCocoa and MVVM, an Introduction