關于ReactiveCocoa
ReactiveCocoa是iOS環(huán)境下的一個函數(shù)式響應式編程框架。函數(shù)式響應式編程(Functional Reactive Programming, FRP)這個概念由微軟的牛X團隊提出,ReactiveCocoa是受其啟發(fā)而誕生的框架,應用范圍非常廣泛。
ReactiveCocoa至今已經(jīng)發(fā)展出第4個版本了,而本文所針對的ReactiveCocoa版本是v2.5;照常例,下文將使用RAC代替ReactiveCocoa。
P.S: ReactiveCocoa 3.0已經(jīng)放棄對iOS 7的支持,因此如果支持iOS 7,必須得使用更低的版本。因此,若想通過CocoaPods安裝ReactiveCocoa v2.5,需要指明platform是低于v8.0的版本,譬如platform :ios, '7.0'。如果使用Swift語言,RxSwift似乎是更好的選擇。
在本文中,理解Signal是重頭戲,個人認為,結合Sequence理解信號(Signal)比較容易一些,因此會花一些篇幅闡述Sequence;除此之外,還有必要介紹高階函數(shù)和函數(shù)式編程。
P.S: Sequence在RAC中的地位越來越低,在Swift環(huán)境中,干脆被干掉了。
高階函數(shù)和函數(shù)式編程
從維基百科的解釋來看,一個高階函數(shù)需要滿足如下兩個條件:
- 一個或者多個函數(shù)作為輸入;
- 有一個函數(shù)作為輸出。
P.S: 維基百科代表不了權威,至少在高階函數(shù)這個問題上,網(wǎng)友存在分歧,有的人認為這兩個條件滿足一個即可;另一部分人認為二者皆不可缺。為了配合我所參考的資料 –iOS的函數(shù)響應型編程– 的說法,我只好選擇后者。
有必要在了解ReactiveCocoa之前認識一下函數(shù)式編程,可以參考函數(shù)式編程初探和用RXCollections進行函數(shù)式編程。
流和序列
RAC有一個核心概念叫流(Stream),它是data的序列化抽象。下文提到的LimBoy把Signal比喻成水管,我認為完全可以把Stream也比喻成水管,data就好像流淌在水管中的玻璃球,玻璃球直徑和水管內徑相仿,只能依次流過。以我們當前的認知水平,data的序列化就好像是一個數(shù)組或者列表。Data在序列中依次被排好序,它們能像水管中的玻璃球一樣流出來。
P.S: 「水管」和「玻璃球」比喻,我所知道的出處是LimBoy,下文再解釋Signal時會再次說明。
在RAC中,使用類RACStream抽象Stream,該類是個抽象類,本身不能被用來定義實例,用得更多的是它的兩個子類:RACSequence和RACSignal。換句話說,在RAC中,有兩種特定的流:序列(RACSequence)和信號(RACSignal)。
可以使用RAC為NSArray定義的category方法rac_sequence,將數(shù)組橋接為一個序列:
NSArray*array =@[@1,@2,@3,@4];
RACSequence*sequence1 = [arrayrac_sequence];
可以對生成的sequence1進行map、filter等處理,得到一個新的序列,前者對序列中的每個data進行處理,后者將序列中的data進行匹配過濾。下面例子先進行map處理,對序列中的data進行平方處理,得到一個新的序列sequence2,然后再對新序列進行filter處理,將偶數(shù)給剔除掉,得到sequence3,最后,再將sequence3還原為NSArray,并將所有元素給打印出來:
RACSequence *sequence2 = [sequence1 map:^id(NSNumber * value) {
return [NSNumber numberWithInteger:value.integerValue*value.integerValue];
}];
RACSequence *sequence3 = [sequence2 filter:^BOOL(NSNumber * value) {
return value.integerValue % 2 == 1;
}];
NSLog(@"%@", sequence3.array);
// print: (1, 9)
顯然,上述的兩個步驟其實可以鏈式串起來,這樣至少可以將sequence2這個臨時變量給省掉。
到了這里,應該對Sequence有了基本的理解了。
P.S: 文筆不佳,閱讀《iOS的函數(shù)響應型編程》的前兩章效果會更好。
P.S: 除了NSArray,還可以橋接為RACSequence的類型包括:NSDictionary、NSEnumerator、NSIndexSet、NSSet、RACSequence、RACTuple(RAC定義的一種類型,和其他語言譬如Swift中的Tuple類似)。
除了上面提到的map、filter,RAC的Stream還定義了其他很多基本操作:
- (instancetype)flattenMap:(RACStream * (^)(id value))block;
- (instancetype)flatten;
- (instancetype)map:(id (^)(id value))block;
- (instancetype)filter:(BOOL (^)(id value))block;
- (instancetype)ignore:(id)value;
- (instancetype)skip:(NSUInteger)skipCount;
- (instancetype)take:(NSUInteger)count;
+ (instancetype)zip:(id<NSFastEnumeration>)streams;
如上只是其中一部分,更過基本操作詳見RACStream.h。需要說明的是,上述的基本操作都是建立在如下API基礎之上,RACStream并未實現(xiàn)這些API,得由子類(即RACSequence和RACSignal)自己實現(xiàn)。
+ (instancetype)empty;
+ (instancetype)return:(id)value;
- (instancetype)bind:(RACStreamBindBlock (^)(void))block;
- (instancetype)concat:(RACStream *)stream;
- (instancetype)zipWith:(RACStream *)stream;
P.S: RACStream定義的如上5個抽象方法,sunnyxx在其博客Reactive Cocoa Tutorial [2] = 百變RACStream里已有比較好的說明,可以參考一下。
如何看待上述的基本方法呢?這些方法(無論是類方法還是實例方法)都返回RACStream對象,這意味著可以在它們的基礎上進行鏈式調用,事實上達成了函數(shù)式編程的目的。
想想數(shù)學分支中的代數(shù),最基礎的運算無非是加減,在加減的基礎上引出了乘除,然后有了各種各樣更復雜的數(shù)學運算,譬如求導、微分、積分、卷積等等。對于Stream也一樣,在理解這些基本操作后,我們就可以基于鏈式調用實現(xiàn)各種復雜的邏輯。
至于這些操作的具體意義及用法,本文先略過,以后再說。
信號
在RAC中,Signal也是一種Stream,可以被綁定和傳遞。把Sequence想象成Stream并不是很難,但把Signal理解成Stream還是蠻有挑戰(zhàn)的。
如何理解Signal呢?能力有限,我也沒有更好的表述。如下是一段在ReactiveCocoa的中文世界里被廣泛傳播的解釋:
可以把信號想象成水龍頭,只不過里面不是水,而是玻璃球(data),直徑跟水管的內徑一樣,這樣就能保證玻璃球是依次排列,不會出現(xiàn)并排的情況(數(shù)據(jù)都是線性處理的,不會出現(xiàn)并發(fā)情況)。水龍頭的開關默認是關的,除非有了接收方(subscriber),才會打開。這樣只要有新的玻璃球進來,就會自動傳送給接收方??梢栽谒堫^上加一個過濾嘴(filter),不符合的不讓通過,也可以加一個改動裝置,把球改變成符合自己的需求(map)。也可以把多個水龍頭合并成一個新的水龍頭(combineLatest:reduce:),這樣只要其中的一個水龍頭有玻璃球出來,這個新合并的水龍頭就會得到這個球。
P.S: 這段摘自LimBoy的博文ReactiveCocoa與Functional Reactive Programming。
Sequence v.s Signal
Sequence和Signal都是Stream,但它們是不同類型的流。前者是pull-driven(拉驅動),后者是push-driven(推驅動)。所謂pull-driven,可以類比獲取網(wǎng)頁的方式,發(fā)起一個正確的HTTP請求,我們總會得到一些數(shù)據(jù),因為數(shù)據(jù)就在服務端的數(shù)據(jù)庫中躺著;而push-driven,可以類比推送(Push),數(shù)據(jù)并不是隨時都有的,客戶端也不知道什么時候該去獲取,只能與服務端保持長連接,當服務端有新數(shù)據(jù)時,就主動推送(Push)過來。
對于初學者,理解pull-driven和push-driven這兩個名詞不是很容易,但理解Sequence和Signal的區(qū)別還是不難:對于Sequence而言,在它被創(chuàng)立之初,其中的data(玻璃球)是被確定的,可以從流中把它們一個一個查詢出來;但對于Signal而言,在它(水管)被創(chuàng)立的時候,其中是沒有data(玻璃球)的,data是之后在某個時刻(譬如notification發(fā)生時、網(wǎng)絡請求完成時)才被放入的。
除了data驅動方式不同,Sequence和Signal所傳遞的data類型還不同,Sequence傳遞的是對象,Signal傳遞的是事件,無論是對象還是事件,在本文中都以data概述。
P.S: RACSequence和RACSignal可以互相轉化,詳見Signal。
Signal和Subscriber
對于Sequence而言,在其創(chuàng)立之初,其中的data就是確定的,經(jīng)過一系列的操作(譬如map、filter)便可將同步將結果給取出來。
但對于Signal,不曉得什么時候才有data被放進去,顯然不能同步等待處理結果。因而需要有一種機制來解決這個問題。
Subscription就是來解決這個問題的,Subscriber是Subscription中的核心概念,RAC定義RACSubscriber來描述它。關于Subscription和Subscriber,官方文檔已經(jīng)有非常清晰的表述,詳見這里和這里。
P.S: 當把信號理解成流時,「signal」這個名詞怎么看都覺得別扭,但當它和subscriber搭配時,卻又顯得那么和諧。
在描述signal和subscriber的關系時,Limboy使用插座和插頭分別來類比它們,插座(signal)負責取電,插頭(subscriber)負責使用電,一個插座可以插任意數(shù)量的插頭。當一個插座沒有插頭時,什么都不會干,處于冷(cold)狀態(tài),只有插了插頭才會去獲取電,此時處于熱(hot)狀態(tài)。
上文已經(jīng)提到Signal傳遞的data是event,它所傳遞的event包括3種:值事件、完成事件和錯誤事件。其中在傳遞值事件時,可以攜帶數(shù)據(jù)。
落實到代碼層面,傳遞值事件、完成事件以及錯誤事件的本質就是向subscriber發(fā)送sendNext:、sendComplete以及sendError:消息,如下代碼可以簡單描述它們的關系:
// 1. 創(chuàng)建信號(冷信號)
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// block在什么時候被調用: 當信號被訂閱時調用;
// 3. 模擬data(值事件)的產生
[subscriber sendNext:@"Hello"];
return nil;
}];
// 2. 訂閱信號(冷信號變熱信號)
[signal subscribeNext:^(id x) {
// block什么時候被調用:當subscriber接收到sendNext:消息時;
NSLog(@"%@", x);
}];
上述代碼中,步驟2在內部實現(xiàn)里創(chuàng)建一個RACSubscriber對象,該對象會被傳入到步驟3所對應的block中。
冷信號和熱信號
沒有被訂閱的信號被稱為冷信號,冷信號默認情況下什么都不干,換句話說,冷信號的subscription block永遠都不會被執(zhí)行,譬如:
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"triggered");
[subscriber sendNext:@"hello, next"];
[subscriber sendCompleted];
return nil;
}];
這段代碼創(chuàng)建了一個signal,但因為沒有被subscribed,所以什么也不會發(fā)生。代碼中使用類方法createSignal:創(chuàng)建一個RACDynamicSignal(RACSignal的子類)對象,后者有一個名為didSubscribe的Block屬性,調用createSignal:傳入的實參block被賦予該屬性,當RACDynamicSignal被訂閱(subscribe)時,會回調該block。
P.S: RAC中的RACSignal以類簇的方式實現(xiàn),有點類似于Foundation中NSString、NSArray等,它定義了很多RACSignal子類,暫時不用理會這些子類,以后的博客中再詳細介紹。
Side Effect
如果某個RACSignal(以RACDynamicSignal為例)被多個subscriber訂閱,那么它的didSubscribe會被多次調用嗎?默認情況下是的,如下:
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// didSubscribe block
NSLog(@"triggered");
[subscriber sendNext:@"test"];
return nil;
}];
[signal subscribeNext:^(id x) {
NSLog(@"subscriber No.1: %@", x);
}];
[signal subscribeNext:^(id x) {
NSLog(@"subscriber No.2: %@", x);
}];
/* prints:
triggered
subscriber No.1: test
triggered
subscriber No.2: test
*/
顯然,didSubscribe被調用了兩次。或許這是你想要的結果,或許不是;更多的時候這不是我們想要的結果,即所謂的副作用(side effect)。如果想要避免這種情況的發(fā)生,可以使用reply方法,它的作用是保證signal只被觸發(fā)一次,然后把sendNext:的value給緩存起來,下一次再有新的subscriber時。直接發(fā)送緩存的value。如下:
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// didSubscribe block
NSLog(@"triggered");
[subscriber sendNext:@"test"];
return nil;
}];
signal = [signal replay];
[signal subscribeNext:^(id x) {
NSLog(@"subscriber No.1: %@", x);
}];
[signal subscribeNext:^(id x) {
NSLog(@"subscriber No.2: %@", x);
}];
/* prints:
triggered
subscriber No.1: test
subscriber No.2: test
*/
P.S: 我對side effects的理解有問題,把它單純想象成應該避免的負面東西,這是不對的。需要有更深刻的理解,得重寫一下!還得思考「side effects里一般放什么樣的代碼?」。
時至今日,ReactiveCocoa已經(jīng)和AFNetWorking一樣,變得非常大眾了,關于它的學習資料也不像前兩年那樣稀缺,如下是我認為質量比較高的學習資料:
-
LeeBoy的幾篇ReactiveCocoa博文
iOS的函數(shù)響應型編程,非常淺顯易懂。
-
美團點評技術團隊分享的幾篇關于冷信號和熱信號的博客
-
Sunnyxx系列博客