一、冷熱信號:
美團(tuán)冷熱信號1
1、熱信號是主動的,即使你沒有訂閱事件,它仍然會時刻推送。
而冷信號是被動的,只有當(dāng)你訂閱的時候,它才會發(fā)送消息。
2、熱信號可以有多個訂閱者,是一對多,信號可以與訂閱者共享信息。
而冷信號只能一對一,當(dāng)有不同的訂閱者,消息會重新完整發(fā)送。
二、為什么要區(qū)分冷、熱信號:
美團(tuán)冷熱信號2
這里面引用的例子很說服力,足見臧老師的功底之深。我決定把例子在這里詳細(xì)的講解下:
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];
}
}];
}];
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];
}];
**我們不妨在demo中實際運(yùn)行一遍,不要只是單純的看代碼。切身體會下這段的問題所在。
分析前先下結(jié)論:
1、
信號只有被訂閱后才會產(chǎn)生值。
2、任何信號變換的本質(zhì)都是依賴bind函數(shù),而bind函數(shù)的實現(xiàn)在上一篇中我們已經(jīng)講過,所以這里直接有的概念就是:任何信號的轉(zhuǎn)換都是對原有信號進(jìn)行訂閱,從而產(chǎn)生新信號。
3、這里一定要注意的是:我們在最外層創(chuàng)建信號后,在內(nèi)部對原始信號進(jìn)行訂閱時,用到的subscriber是組外層信號的訂閱者,也就是只有新創(chuàng)建的信號被訂閱時,我們內(nèi)部才會間接地對原始信號進(jìn)行訂閱。
有了這些前置概念,我們再來看下上面的代碼:
1、fetchData信號被flattenMap之后,會因為title、desc被訂閱從而間接的被訂閱,desc被flattenMap后生成renderedDesc,等到renderedDesc被訂閱后,fetchData會再次被間接訂閱,因此會有三次訂閱的過程,也就是會產(chǎn)生三次網(wǎng)絡(luò)請求。
2、我們看到上述代碼還有一個merge操作,這里會將三個信號merge成為一個新信號
,創(chuàng)建了一個新的信號,在這個信號被訂閱的時候,把它包含的所有信號訂閱。所以我們又得到了額外的3次網(wǎng)絡(luò)請求。
總結(jié):每一次的訂閱都會導(dǎo)致信號被重新執(zhí)行,從而引起6次網(wǎng)絡(luò)請求,而造成這種現(xiàn)象的原因是:fetchData是一個冷信號
。所以每次訂閱都會重新執(zhí)行一次。如果是熱信號,即使被訂閱多次,我們也不會,因為每次訂閱,信號都會被執(zhí)行一次。
這篇博客中也提到了:FP(函數(shù)式編程)以及副作用的相關(guān)概念,這里不在細(xì)說,大家可以自行閱讀。
三、如何處理冷、熱信號
RACSubject 和RACReplaySubject :
1、RACSubject:
1.1、RACSubject是熱信號,他的訂閱者在訂閱后,不會收到在訂閱前發(fā)送的信號值,只會收到從訂閱時間點開始后產(chǎn)生的信號值。
1.2、多個訂閱者可以共享信號值。
2、RACSubject訂閱處理邏輯
可以看到,訂閱前發(fā)送的信號訂閱者都不會收到。
3、RACReplaySubject:是RACSubject子類,訂閱者在訂閱它之后會先將之前已經(jīng)發(fā)送的信號,快速發(fā)送一遍給訂閱者。然后再回到當(dāng)前的現(xiàn)實,等待下一個信號的到來。
上面的博客中臧老師形象的用時空穿越的例子來描述:舉個生動的例子,就好像科幻電影里面主人公穿越時間線后會先把所有的回憶快速閃過再來到現(xiàn)實一樣。(見《X戰(zhàn)警:逆轉(zhuǎn)未來》、《蝴蝶效應(yīng)》)所以我們也有理由認(rèn)定replaySubject天然也是熱信號。
這一點不得不佩服,能把抽象的知識講的富有畫面感,不得不說是只有對該領(lǐng)域有了充分且足夠深入的理解才能達(dá)到這種境界,佩服!
4、RACReplaySubject的訂閱時處理邏輯如下:
5、結(jié)論
RACSubject
及其子類
是熱信號。
RACSignal
排除RACSubject
類以外的是冷信號。
四、冷信號轉(zhuǎn)熱信號
1、冷信號轉(zhuǎn)換成熱信號的本質(zhì)
冷信號轉(zhuǎn)換成熱信號的本質(zhì):就是使用一個subject訂閱原始信號,讓其他訂閱者訂閱這個subject,這個subject就是熱信號。
2、代碼實現(xiàn):
- (void)coldSignalTransferHotSignal {
//1、創(chuàng)建冷信號
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"=====cold signal subscribered");
[[RACScheduler mainThreadScheduler] afterDelay:1.0 schedule:^{
[subscriber sendNext:@"AAA"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:3.0 schedule:^{
[subscriber sendNext:@"BBB"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:5.0 schedule:^{
[subscriber sendCompleted];
}];
return nil;
}];
//2、創(chuàng)建subject,并訂閱冷信號
RACSubject *subject = [RACSubject subject];
NSLog(@"=======subject 被創(chuàng)建");
[[RACScheduler mainThreadScheduler] afterDelay:2.0 schedule:^{
[coldSignal subscribe:subject]; //放在主線程中訂閱
}];
//3、其他訂閱者訂閱subject
[subject subscribeNext:^(id x) {
NSLog(@"====第一個訂閱者,收到信號值:%@",x);
}];
[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{
[subject subscribeNext:^(id x) {
NSLog(@"=====第二個訂閱者,收到信號值:%@",x);
}];
}];
}
3、RAC官方給出的信號轉(zhuǎn)換API:
當(dāng)然,使用這種RACSubject來訂閱冷信號得到熱信號的方式仍有一些小的瑕疵。例如subject的訂閱者提前終止了訂閱,而subject并不能終止對coldSignal的訂閱。
所以在RAC庫中對于冷信號轉(zhuǎn)化成熱信號有如下標(biāo)準(zhǔn)的封裝
- (RACMulticastConnection *)publish;
- (RACMulticastConnection *)multicast:(RACSubject *)subject;
- (RACSignal *)replay;
- (RACSignal *)replayLast;
- (RACSignal *)replayLazily;
這5個方法中,最為重要的就是- (RACMulticastConnection *)multicast:(RACSubject *)subject;
這個方法了,其他幾個方法也是間接調(diào)用它的。至于multiCast的實現(xiàn),可參閱博客,原文講的很好。
4、使用multicast: 來完善冷熱信號轉(zhuǎn)換的本質(zhì):
- (void)multiCast {
//1、創(chuàng)建冷信號
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"=====cold signal subscribered");
[[RACScheduler mainThreadScheduler] afterDelay:1.0 schedule:^{
[subscriber sendNext:@"AAA"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:3.0 schedule:^{
[subscriber sendNext:@"BBB"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:5.0 schedule:^{
[subscriber sendCompleted];
}];
return nil;
}];
//使用multicast:將冷信號轉(zhuǎn)換成熱信號
RACSubject *subject = [RACSubject subject];
RACMulticastConnection *connection = [coldSignal multicast:subject];
/*
//1、使用connect
RACSignal *hotSignal = connection.signal;
//主動觸發(fā)connect
[[RACScheduler mainThreadScheduler] afterDelay:2.0 schedule:^{
[connection connect];
}];
*/
//2、使用autoconnect
RACSignal *hotSignal = connection.autoconnect;
//訂閱熱信號
[hotSignal subscribeNext:^(id x) {
NSLog(@"====第一個訂閱者,收到信號值:%@",x);
}];
[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{ //4s后開始訂閱
[hotSignal subscribeNext:^(id x) {
NSLog(@"====第二個訂閱者,收到信號值:%@",x);
}];
}];
}
注意:看publish的源碼會發(fā)現(xiàn),其實publish就是做了上面的工作。
5、replay、replayLatest、replayLazily對比
- (RACSignal *)replay就是用RACReplaySubject來作為subject,并立即執(zhí)行connect操作,返回connection.signal。其作用是上面提到的replay功能,即后來的訂閱者可以收到歷史值。
- (RACSignal *)replayLast就是用容量為1的RACReplaySubject來替換- (RACSignal *)replay的subject。其作用是使后來訂閱者只收到:
訂閱者訂閱前信號發(fā)送的最后一次歷史值。
- (RACSignal *)replayLazily和- (RACSignal *)replay的區(qū)別就是:
replayLazily只有在第一次訂閱的時候才訂閱sourceSignal。簡單講:直到訂閱的時候才真正創(chuàng)建一個信號,源信號的訂閱代碼才開始執(zhí)行
具體看例子:
- (void)comparisonSignal {
//創(chuàng)建冷信號
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"222 冷信號(原始信號)被訂閱");
[[RACScheduler mainThreadScheduler] afterDelay:1.0 schedule:^{
[subscriber sendNext:@"AAA"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:3.0 schedule:^{
[subscriber sendNext:@"BBB"];
}];
[[RACScheduler mainThreadScheduler] afterDelay:5.0 schedule:^{
[subscriber sendCompleted];
}];
return nil;
}];
//分別使用以下兩種方式轉(zhuǎn)換成熱信號
// RACSignal *hotSignal = [coldSignal replayLazily];
RACSignal *hotSignal = [coldSignal replay];
NSLog(@"111開始訂閱");
//訂閱熱信號
[hotSignal subscribeNext:^(id x) {
NSLog(@"====第一個訂閱者,收到信號值:%@",x);
}];
[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{ //4s后開始訂閱
[hotSignal subscribeNext:^(id x) {
NSLog(@"====第二個訂閱者,收到信號值:%@",x);
}];
}];
}
*********************************************************************
使用replay的輸出結(jié)果:
2017-12-19 16:22:00.338530+0800 222 冷信號(原始信號)被訂閱
2017-12-19 16:22:00.338857+0800 111開始訂閱
使用replayLazily的輸出結(jié)果:
2017-12-19 16:23:23.577210+0800 111開始訂閱
2017-12-19 16:23:23.577920+0800 222 冷信號(原始信號)被訂閱
我們可以看到,replayLazily 只會在訂閱時,才會去創(chuàng)建信號,源信號的訂閱代碼才會被執(zhí)行。
6、回到第二篇博客中的例子上,我們?yōu)榱吮苊饩W(wǎng)絡(luò)請求執(zhí)行多次,保證它只會執(zhí)行一次,我們需要將冷信號轉(zhuǎn)換成熱信號(熱信號不會因為訂閱者的訂閱,重新播放)。改動后的代碼如下:
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]; // 使用replayLazily 轉(zhuǎn)換成熱信號,而且保證網(wǎng)絡(luò)請求的代碼是直到訂閱才去執(zhí)行。
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];
}];
當(dāng)然臧老師還提到:
例如將fetchData轉(zhuǎn)換為title的block會執(zhí)行多次,將fetchData轉(zhuǎn)換為desc的block也會執(zhí)行多次。但是由于這些block都是無副作用的,計算量并不大,可以忽略不計。如果計算量大的,也需要對中間的信號進(jìn)行熱信號的轉(zhuǎn)換。不過請不要忽略冷熱信號的轉(zhuǎn)換本身也是有計算代價的。
這里我們也可以總結(jié)下:當(dāng)模塊代碼會重復(fù)執(zhí)行多次時,我們想要避免這種情況,可以采取轉(zhuǎn)換成熱信號的方式。但是假如該代碼重復(fù)執(zhí)行不會產(chǎn)生副作用,那么我們則可以允許這種情況的存在。