ReactiveCocoa學習筆記(四):「RAC微博」基礎使用手冊

本文簡單介紹了ReactiveCocoa的基礎用法,希望讀完能對這個框架的使用有一個大概的了解。

前面幾篇文章,我們研究了ReactiveCocoa(以下簡稱RAC)的起源和思想。網絡上介紹RAC的使用的好文甚多,在文末的Reference中標出了一些以供參考。RAC這個開源庫本身也是十分良心,代碼注釋十分齊全,因此這一篇文章不做太多的擴展,僅談一談RAC的基礎使用方法。

RACSignal

上文中說到,函數響應式編程中,將各種通信機制所需要解決的「輸入」與「輸出」的異步關系抽象成了事件/時間驅動的值流,并通過monad使其支持了函數式編程的特性。而在RAC中,這個東西就是RACStream,開發過程中我們并不是直接使用它,而是其子類——RACSignalRACSequence。這一節,講講RACSignal

Signal,顧名思義,代表一個信號,可以源源不斷地給你傳遞信息。這樣就好理解RACSignal代表著「隨時間變化的值流」,這里的值,就包含了將來即將到來的「輸入」。打個比方,一個微博博主便是一個「Signal」,只要沒被封號,你就會知道將來他會一直發出消息。如果關注了這個博主,一旦他開始發消息,新消息會被自動推送到你的設備,因此說RACSignal是一個Push-Driven的值流。

那么,RACSignal博主會發出什么消息呢?一個RACSignal傳遞的值分為三類:

  1. Next。「Next」代表著一個新的值,一條新的微博。只要這個博主是活躍的,他就會源源不斷地發微博。
  2. Error。「Error」則代表著這個Signal出了什么問題,發出了一個代表「錯誤」的信號。發送出「Error」也就意味著這個Signal的消息到此為止了。比如這位博主被封號了,他就會給你發一條微博,上面寫著「404Error」,你就知道他再也不會發微博了……
  3. Completed。代表一個Signal完成了自己的全部信息發送。比如某天這個博主想退出微博了,于是發出最后一條微博——「ByeBye粉絲們」。這就是「Completed」。

一個Signal的信息流,都是由若干個「Next」,加上一個代表終結的「Error」或「Completed」組成的。

這些值都是從哪里來的呢?一個Signal所發出的信息主要來源有兩種:

  1. 手動創建一個信號時定義它發出的信息。這就好像是一位原創博主,每條微博都是他自己寫的:
// 代碼1
RACSignal *blogSignalA =
[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    [subscriber sendNext:@"blog1"];
    [subscriber sendNext:@"blog2"];
    [subscriber sendNext:@"blog3"];
    [subscriber sendCompleted];
    return nil;
}];
  1. 由其他通信機制生成一個信號時,在其他通信機制產生輸入時發出消息。比如某些大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這位博主,他發出的blog1blog2等等微博都會推送到我們,由我們進行處理。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支持各種函數式的操作,例如mapreducefilter等等。這可以讓我們方便地對原始信號傳輸出的信息進行一步步加工,最終得到我們所需要的值,這就是「函數性」賦予的利器:

// 代碼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來解決這個問題。

RACSubjectRACSignal的子類。是不同于只會根據腳本發送固定信號的RACSignalRACSubject能夠由我們程序控制,在任何時候主動發送新的值。這有點類似于不可變數組和可變數組的概念。可以想象這樣一種情景,我們要在原有的舊代碼里利用RAC完成一些功能,那么可以利用RACSubject,在老代碼中間手動控制其發送出信號。因此,官方稱RACSubject為「most helpful in bridging the non-RAC world to RAC」。

RACSubject是一個「熱信號」,它在內部維護了一個「訂閱者」的統計數組。每當產生新的訂閱行為的時候,它只是簡單地將這個「訂閱者」添加進自己維護的數組中。等到發出信號的時候,會遍歷該數組,向其中所有的「訂閱者」發送該信號。也就是說,它不會管有沒有訂閱行為,而只是自己發自己的信號。而訂閱之后,也只能收到它以后發出的信號。嗯,這樣的行為才是一個活生生的大V博主,而不是冰冷的腳本嘛!

同時,RACSubject還是一個「訂閱者」,它實現了RACSubscriber協議,也就是說,它可以訂閱一個「RACSignal」。當接受到「RACSignal」發送的信號的時候,它會遍歷其內部的「訂閱者」數組,將自己接收到的信號轉發給每一個「訂閱者」。也就是說,RACSubject充當了一個中間轉發者的角色。這樣既保證了對原始信號只訂閱一次,從而可以消除副作用的影響,又保證了外界多個訂閱者的正常行為。

<div align=center>


image

</div>

在RAC中,這種關系是由RACMulticastConnection類以及multicastconnect操作實現的:

// 代碼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 Asubscribe 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在執行操作后,已將該操作生成的RACSignalRACReplaySubject進行了multicast,所以不用擔心內部操作所包含的副作用問題。

可以看出,RACCommandUIButton的作用是很相似的:都是用于執行某個操作。事實上,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官方更推薦使用namelog進行調試。
  • 學習曲線還是比較陡峭的,需要理解函數響應式編程的思想,以及學習RAC的基本知識。調bug的時候甚至還需要充分了解RAC內部原理。

因此,是否使用RAC到實際的大型項目中,這是個見仁見智的話題。但是一些小項目或是自己學習、研究、使用,還是十分有價值的。


Reference

霜神解讀RAC源碼系列
美團點評技術博客的RAC系列
Draveness解讀的RAC源碼系列
ReactiveCocoa and MVVM, an Introduction

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容