细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号
第一篇文章中我們介紹了冷信號與熱信號的概念,前一篇文章我們也討論了為什么要區分冷信號與熱信號,下面我會先為大家揭曉熱信號的本質,再給出冷信號轉換成熱信號的方法。
揭示熱信號的本質
在ReactiveCocoa中,究竟什么才是熱信號呢?冷信號是比較常見的,map一下就會得到一個冷信號。但在RAC中,好像并沒有“hot signal”這個單獨的說法。原來在RAC的世界中,所有的熱信號都屬于一個類——RACSubject。接下來我們來看看究竟它為什么這么“神奇”。
在RAC2.5文檔的框架概述中,有著這樣一段描述:
A subject, represented by the RACSubject class, is a signal that can be manually controlled.
Subjects can be thought of as the “mutable” variant of a signal, much like NSMutableArray is for NSArray. They are extremely useful for bridging non-RAC code into the world of signals.
For example, instead of handling application logic in block callbacks, the blocks can simply send events to a shared subject instead. The subject can then be returned as a RACSignal, hiding the implementation detail of the callbacks.
Some subjects offer additional behaviors as well. In particular, RACReplaySubject can be used to buffer events for future subscribers, like when a network request finishes before anything is ready to handle the result.
從這段描述中,我們可以發現Subject具備如下三個特點:
從第三個特點來看,Subject具備為未來訂閱者緩沖事件的能力,那也就說明它是自身是有狀態的。根據上文的介紹,Subject是符合熱信號的特點的。為了驗證它,我們再來做個簡單實驗:
RACSubject *subject = [RACSubject subject];RACSubject *replaySubject = [RACReplaySubject subject];[[RACScheduler mainThreadScheduler] afterDelay:0.1 schedule:^{// Subscriber 1[subject subscribeNext:^(id x) {NSLog(@"Subscriber 1 get a next value: %@ from subject", x);}];[replaySubject subscribeNext:^(id x) {NSLog(@"Subscriber 1 get a next value: %@ from replay subject", x);}];// Subscriber 2[subject subscribeNext:^(id x) {NSLog(@"Subscriber 2 get a next value: %@ from subject", x);}];[replaySubject subscribeNext:^(id x) {NSLog(@"Subscriber 2 get a next value: %@ from replay subject", x);}];}];[[RACScheduler mainThreadScheduler] afterDelay:1 schedule:^{[subject sendNext:@"send package 1"];[replaySubject sendNext:@"send package 1"];}];[[RACScheduler mainThreadScheduler] afterDelay:1.1 schedule:^{// Subscriber 3[subject subscribeNext:^(id x) {NSLog(@"Subscriber 3 get a next value: %@ from subject", x);}];[replaySubject subscribeNext:^(id x) {NSLog(@"Subscriber 3 get a next value: %@ from replay subject", x);}];// Subscriber 4[subject subscribeNext:^(id x) {NSLog(@"Subscriber 4 get a next value: %@ from subject", x);}];[replaySubject subscribeNext:^(id x) {NSLog(@"Subscriber 4 get a next value: %@ from replay subject", x);}];}];[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{[subject sendNext:@"send package 2"];[replaySubject sendNext:@"send package 2"];}];按照時間線來解讀一下上述代碼:
接下來看一下輸出的結果:
2015-09-28 13:35:22.855 RACDemos[13646:1269269] Start 2015-09-28 13:35:23.856 RACDemos[13646:1269269] Subscriber 1 get a next value: send package 1 from subject 2015-09-28 13:35:23.856 RACDemos[13646:1269269] Subscriber 2 get a next value: send package 1 from subject 2015-09-28 13:35:23.857 RACDemos[13646:1269269] Subscriber 1 get a next value: send package 1 from replay subject 2015-09-28 13:35:23.857 RACDemos[13646:1269269] Subscriber 2 get a next value: send package 1 from replay subject 2015-09-28 13:35:24.059 RACDemos[13646:1269269] Subscriber 3 get a next value: send package 1 from replay subject 2015-09-28 13:35:24.059 RACDemos[13646:1269269] Subscriber 4 get a next value: send package 1 from replay subject 2015-09-28 13:35:25.039 RACDemos[13646:1269269] Subscriber 1 get a next value: send package 2 from subject 2015-09-28 13:35:25.039 RACDemos[13646:1269269] Subscriber 2 get a next value: send package 2 from subject 2015-09-28 13:35:25.039 RACDemos[13646:1269269] Subscriber 3 get a next value: send package 2 from subject 2015-09-28 13:35:25.040 RACDemos[13646:1269269] Subscriber 4 get a next value: send package 2 from subject 2015-09-28 13:35:25.040 RACDemos[13646:1269269] Subscriber 1 get a next value: send package 2 from replay subject 2015-09-28 13:35:25.040 RACDemos[13646:1269269] Subscriber 2 get a next value: send package 2 from replay subject 2015-09-28 13:35:25.040 RACDemos[13646:1269269] Subscriber 3 get a next value: send package 2 from replay subject 2015-09-28 13:35:25.040 RACDemos[13646:1269269] Subscriber 4 get a next value: send package 2 from replay subject結合結果可以分析出如下內容:
只關注subject,根據時間線,我們可以得到下圖:
經過觀察不難發現,4個訂閱者實際上是共享subject的,一旦這個subject發送了值,當前的訂閱者就會同時接收到。由于Subscriber 3與Subscriber 4的訂閱時間稍晚,所以錯過了第一次值的發送。這與冷信號是截然不同的反應。冷信號的圖類似下圖:
對比上面兩張圖,是不是可以發現,subject類似“直播”,錯過了就不再處理。而signal類似“點播”,每次訂閱都會從頭開始。所以我們有理由認定subject天然就是熱信號。
下面再來看看replaySubject,根據時間線,我們能得到另一張圖:
將圖3與圖1對比會發現,Subscriber 3與Subscriber 4在訂閱后馬上接收到了“歷史值”。對于Subscriber 3和Subscriber 4來說,它們只關心“歷史的值”而不關心“歷史的時間線”,因為實際上1與2是間隔1s發送的,但是它們接收到的顯然不是。舉個生動的例子,就好像科幻電影里面主人公穿越時間線后會先把所有的回憶快速閃過再來到現實一樣。(見《X戰警:逆轉未來》、《蝴蝶效應》)所以我們也有理由認定replaySubject天然也是熱信號。
看到這里,我們終于揭開了熱信號的面紗,結論就是:
如何將一個冷信號轉化成熱信號——廣播
冷信號與熱信號的本質區別在于是否保持狀態,冷信號的多次訂閱是不保持狀態的,而熱信號的多次訂閱可以保持狀態。所以一種將冷信號轉換為熱信號的方法就是,將冷信號訂閱,訂閱到的每一個時間通過RACSbuject發送出去,其他訂閱者只訂閱這個RACSubject。
觀察下面的代碼:
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {NSLog(@"Cold signal be subscribed.");[[RACScheduler mainThreadScheduler] afterDelay:1.5 schedule:^{[subscriber sendNext:@"A"];}];[[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{[subscriber sendNext:@"B"];}];[[RACScheduler mainThreadScheduler] afterDelay:5 schedule:^{[subscriber sendCompleted];}];return nil;}];RACSubject *subject = [RACSubject subject];NSLog(@"Subject created.");[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{[coldSignal subscribe:subject];}];[subject subscribeNext:^(id x) {NSLog(@"Subscriber 1 recieve value:%@.", x);}];[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{[subject subscribeNext:^(id x) {NSLog(@"Subscriber 2 recieve value:%@.", x);}];執行順序是這樣的:
如果所料不錯的話,通過訂閱這個subject并不會引起coldSignal重復執行block的內容。我們來看下結果:
2015-09-28 19:36:45.703 RACDemos[14110:1556061] Subject created. 2015-09-28 19:36:47.705 RACDemos[14110:1556061] Cold signal be subscribed. 2015-09-28 19:36:49.331 RACDemos[14110:1556061] Subscriber 1 recieve value:A. 2015-09-28 19:36:50.999 RACDemos[14110:1556061] Subscriber 1 recieve value:B. 2015-09-28 19:36:50.999 RACDemos[14110:1556061] Subscriber 2 recieve value:B.參考時間線,會得到下圖:
不難發現其中的幾個重點: 1. subject是從一開始就創建好的,等到2s后便開始訂閱coldSignal。 2. Subscriber 1是subject創建后就開始訂閱的,但是第一個接收時間與subject接收coldSignal第一個值的時間是一樣的。 3. Subscriber 2是subject創建4s后開始訂閱的,所以只能接收到第二個值。
通過觀察可以確定,subject就是coldSignal轉化的熱信號。所以使用RACSubject來將冷信號轉化為熱信號是可行的。
當然,使用這種RACSubject來訂閱冷信號得到熱信號的方式仍有一些小的瑕疵。例如subject的訂閱者提前終止了訂閱,而subject并不能終止對coldSignal的訂閱。(RACDisposable是一個比較大的話題,我計劃在其他的文章中詳細闡述它,也希望感興趣的同學自己來理解。)所以在RAC庫中對于冷信號轉化成熱信號有如下標準的封裝:
- (RACMulticastConnection *)publish; - (RACMulticastConnection *)multicast:(RACSubject *)subject; - (RACSignal *)replay; - (RACSignal *)replayLast; - (RACSignal *)replayLazily;這5個方法中,最為重要的就是- (RACMulticastConnection *)multicast:(RACSubject *)subject;這個方法了,其他幾個方法也是間接調用它的。我們來看看它的實現:
/// implementation RACSignal (Operations) - (RACMulticastConnection *)multicast:(RACSubject *)subject {[subject setNameWithFormat:@"[%@] -multicast: %@", self.name, subject.name];RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject];return connection; }/// implementation RACMulticastConnection- (id)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject {NSCParameterAssert(source != nil);NSCParameterAssert(subject != nil);self = [super init];if (self == nil) return nil;_sourceSignal = source;_serialDisposable = [[RACSerialDisposable alloc] init];_signal = subject;return self; }#pragma mark Connecting- (RACDisposable *)connect {BOOL shouldConnect = OSAtomicCompareAndSwap32Barrier(0, 1, &_hasConnected);if (shouldConnect) {self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal];}return self.serialDisposable; }- (RACSignal *)autoconnect {__block volatile int32_t subscriberCount = 0;return [[RACSignalcreateSignal:^(id<RACSubscriber> subscriber) {OSAtomicIncrement32Barrier(&subscriberCount);RACDisposable *subscriptionDisposable = [self.signal subscribe:subscriber];RACDisposable *connectionDisposable = [self connect];return [RACDisposable disposableWithBlock:^{[subscriptionDisposable dispose];if (OSAtomicDecrement32Barrier(&subscriberCount) == 0) {[connectionDisposable dispose];}}];}]setNameWithFormat:@"[%@] -autoconnect", self.signal.name]; }雖然代碼比較短但不是很好懂,大概來說明一下:
由于RAC是一個線程安全的框架,所以好奇的同學可以了解下“OSAtomic*”這一系列的原子操作。拋開這些應該不難理解上述代碼。
了解源碼之后,這個方法的正確使用就清楚了,應該像這樣:
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {NSLog(@"Cold signal be subscribed.");[[RACScheduler mainThreadScheduler] afterDelay:1.5 schedule:^{[subscriber sendNext:@"A"];}];[[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{[subscriber sendNext:@"B"];}];[[RACScheduler mainThreadScheduler] afterDelay:5 schedule:^{[subscriber sendCompleted];}];return nil;}];RACSubject *subject = [RACSubject subject];NSLog(@"Subject created.");RACMulticastConnection *multicastConnection = [coldSignal multicast:subject];RACSignal *hotSignal = multicastConnection.signal;[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{[multicastConnection connect];}];[hotSignal subscribeNext:^(id x) {NSLog(@"Subscribe 1 recieve value:%@.", x);}];[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{[hotSignal subscribeNext:^(id x) {NSLog(@"Subscribe 2 recieve value:%@.", x);}];}];或者這樣:
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {NSLog(@"Cold signal be subscribed.");[[RACScheduler mainThreadScheduler] afterDelay:1.5 schedule:^{[subscriber sendNext:@"A"];}];[[RACScheduler mainThreadScheduler] afterDelay:3 schedule:^{[subscriber sendNext:@"B"];}];[[RACScheduler mainThreadScheduler] afterDelay:5 schedule:^{[subscriber sendCompleted];}];return nil;}];RACSubject *subject = [RACSubject subject];NSLog(@"Subject created.");RACMulticastConnection *multicastConnection = [coldSignal multicast:subject];RACSignal *hotSignal = multicastConnection.autoconnect;[[RACScheduler mainThreadScheduler] afterDelay:2 schedule:^{[hotSignal subscribeNext:^(id x) {NSLog(@"Subscribe 1 recieve value:%@.", x);}];}];[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{[hotSignal subscribeNext:^(id x) {NSLog(@"Subscribe 2 recieve value:%@.", x);}];}];以上的兩種寫法和之前用Subject來傳遞的例子都可以得到相同的結果。
下面再來看看其他幾個方法的實現:
/// implementation RACSignal (Operations) - (RACMulticastConnection *)publish {RACSubject *subject = [[RACSubject subject] setNameWithFormat:@"[%@] -publish", self.name];RACMulticastConnection *connection = [self multicast:subject];return connection; }- (RACSignal *)replay {RACReplaySubject *subject = [[RACReplaySubject subject] setNameWithFormat:@"[%@] -replay", self.name];RACMulticastConnection *connection = [self multicast:subject];[connection connect];return connection.signal; }- (RACSignal *)replayLast {RACReplaySubject *subject = [[RACReplaySubject replaySubjectWithCapacity:1] setNameWithFormat:@"[%@] -replayLast", self.name];RACMulticastConnection *connection = [self multicast:subject];[connection connect];return connection.signal; }- (RACSignal *)replayLazily {RACMulticastConnection *connection = [self multicast:[RACReplaySubject subject]];return [[RACSignaldefer:^{[connection connect];return connection.signal;}]setNameWithFormat:@"[%@] -replayLazily", self.name]; }這幾個方法的實現都相當簡單,只是為了簡化而封裝,具體說明一下:
所以,其實本質仍然是
使用一個Subject來訂閱原始信號,并讓其他訂閱者訂閱這個Subject,這個Subject就是熱信號。
現在再回過來看下之前系列文章第二篇中那個業務場景的例子,其實修改的方法很簡單,就是在網絡獲取的fetchData這個信號后面,增加一個replayLazily變換,就不會出現網絡請求重發6次的問題了。
修改后的代碼如下,大家可以試試:
self.sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://api.xxxx.com"]];self.sessionManager.requestSerializer = [AFJSONRequestSerializer serializer];self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];@weakify(self)RACSignal *fetchData = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {@strongify(self)NSURLSessionDataTask *task = [self.sessionManager GET:@"fetchData" parameters:@{@"someParameter": @"someValue"} success:^(NSURLSessionDataTask *task, id responseObject) {[subscriber sendNext:responseObject];[subscriber sendCompleted];} failure:^(NSURLSessionDataTask *task, NSError *error) {[subscriber sendError:error];}];return [RACDisposable disposableWithBlock:^{if (task.state != NSURLSessionTaskStateCompleted) {[task cancel];}}];}] replayLazily]; // modify here!!RACSignal *title = [fetchData flattenMap:^RACSignal *(NSDictionary *value) {if ([value[@"title"] isKindOfClass:[NSString class]]) {return [RACSignal return:value[@"title"]];} else {return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{@"originData": value}]];}}];RACSignal *desc = [fetchData flattenMap:^RACSignal *(NSDictionary *value) {if ([value[@"desc"] isKindOfClass:[NSString class]]) {return [RACSignal return:value[@"desc"]];} else {return [RACSignal error:[NSError errorWithDomain:@"some error" code:400 userInfo:@{@"originData": value}]];}}];RACSignal *renderedDesc = [desc flattenMap:^RACStream *(NSString *value) {NSError *error = nil;RenderManager *renderManager = [[RenderManager alloc] init];NSAttributedString *rendered = [renderManager renderText:value error:&error];if (error) {return [RACSignal error:error];} else {return [RACSignal return:rendered];}}];RAC(self.someLablel, text) = [[title catchTo:[RACSignal return:@"Error"]] startWith:@"Loading..."];RAC(self.originTextView, text) = [[desc catchTo:[RACSignal return:@"Error"]] startWith:@"Loading..."];RAC(self.renderedTextView, attributedText) = [[renderedDesc catchTo:[RACSignal return:[[NSAttributedString alloc] initWithString:@"Error"]]] startWith:[[NSAttributedString alloc] initWithString:@"Loading..."]];[[RACSignal merge:@[title, desc, renderedDesc]] subscribeError:^(NSError *error) {UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" message:error.domain delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];[alertView show];}];當然,細心的同學會發現這樣修改,仍然有許多計算上的浪費,例如將fetchData轉換為title的block會執行多次,將fetchData轉換為desc的block也會執行多次。但是由于這些block都是無副作用的,計算量并不大,可以忽略不計。如果計算量大的,也需要對中間的信號進行熱信號的轉換。不過請不要忽略冷熱信號的轉換本身也是有計算代價的。
好的,寫到這里,我們終于揭開RAC中冷信號與熱信號的全部面紗,也知道如何使用了。希望這個系列文章可以讓大家更好地了解RAC,避免使用RAC遇到的誤區。謝謝大家。
美團iOS組有很多志同道合的小伙伴,對于各種技術都有著深入的了解,我們熱忱地歡迎一切牛掰的小伙伴加入,共同學習,共同進步。(簡歷請發送到郵箱 liangsi02@meituan.com)
總結
以上是生活随笔為你收集整理的细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 美团点评移动端基础日志库——Logan
- 下一篇: 消息中间件系列(七):如何从0到1设计一