一竞漾、冷熱信號(hào):
美團(tuán)冷熱信號(hào)1
1眯搭、熱信號(hào)是主動(dòng)的,即使你沒有訂閱事件业岁,它仍然會(huì)時(shí)刻推送鳞仙。
而冷信號(hào)是被動(dòng)的,只有當(dāng)你訂閱的時(shí)候笔时,它才會(huì)發(fā)送消息繁扎。
2、熱信號(hào)可以有多個(gè)訂閱者糊闽,是一對(duì)多梳玫,信號(hào)可以與訂閱者共享信息。
而冷信號(hào)只能一對(duì)一右犹,當(dāng)有不同的訂閱者提澎,消息會(huì)重新完整發(fā)送。
二念链、為什么要區(qū)分冷盼忌、熱信號(hào):
美團(tuán)冷熱信號(hào)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中實(shí)際運(yùn)行一遍谦纱,不要只是單純的看代碼。切身體會(huì)下這段的問題所在君编。
分析前先下結(jié)論:
1跨嘉、
信號(hào)只有被訂閱后才會(huì)產(chǎn)生值。
2吃嘿、任何信號(hào)變換的本質(zhì)都是依賴bind函數(shù)祠乃,而bind函數(shù)的實(shí)現(xiàn)在上一篇中我們已經(jīng)講過,所以這里直接有的概念就是:任何信號(hào)的轉(zhuǎn)換都是對(duì)原有信號(hào)進(jìn)行訂閱兑燥,從而產(chǎn)生新信號(hào)亮瓷。
3、這里一定要注意的是:我們?cè)谧钔鈱觿?chuàng)建信號(hào)后降瞳,在內(nèi)部對(duì)原始信號(hào)進(jìn)行訂閱時(shí)嘱支,用到的subscriber是組外層信號(hào)的訂閱者,也就是只有新創(chuàng)建的信號(hào)被訂閱時(shí),我們內(nèi)部才會(huì)間接地對(duì)原始信號(hào)進(jìn)行訂閱除师。
有了這些前置概念赢织,我們?cè)賮砜聪律厦娴拇a:
1、fetchData信號(hào)被flattenMap之后馍盟,會(huì)因?yàn)閠itle于置、desc被訂閱從而間接的被訂閱,desc被flattenMap后生成renderedDesc贞岭,等到renderedDesc被訂閱后八毯,fetchData會(huì)再次被間接訂閱,因此會(huì)有三次訂閱的過程瞄桨,也就是會(huì)產(chǎn)生三次網(wǎng)絡(luò)請(qǐng)求话速。
2、我們看到上述代碼還有一個(gè)merge操作芯侥,這里會(huì)將三個(gè)信號(hào)merge成為一個(gè)新信號(hào)
泊交,創(chuàng)建了一個(gè)新的信號(hào),在這個(gè)信號(hào)被訂閱的時(shí)候柱查,把它包含的所有信號(hào)訂閱廓俭。所以我們又得到了額外的3次網(wǎng)絡(luò)請(qǐng)求。
總結(jié):每一次的訂閱都會(huì)導(dǎo)致信號(hào)被重新執(zhí)行唉工,從而引起6次網(wǎng)絡(luò)請(qǐng)求研乒,而造成這種現(xiàn)象的原因是:fetchData是一個(gè)冷信號(hào)
。所以每次訂閱都會(huì)重新執(zhí)行一次淋硝。如果是熱信號(hào)雹熬,即使被訂閱多次,我們也不會(huì)谣膳,因?yàn)槊看斡嗛喐捅ǎ盘?hào)都會(huì)被執(zhí)行一次。
這篇博客中也提到了:FP(函數(shù)式編程)以及副作用的相關(guān)概念继谚,這里不在細(xì)說烈菌,大家可以自行閱讀。
三犬庇、如何處理冷僧界、熱信號(hào)
RACSubject 和RACReplaySubject :
1侨嘀、RACSubject:
1.1臭挽、RACSubject是熱信號(hào),他的訂閱者在訂閱后咬腕,不會(huì)收到在訂閱前發(fā)送的信號(hào)值欢峰,只會(huì)收到從訂閱時(shí)間點(diǎn)開始后產(chǎn)生的信號(hào)值。
1.2、多個(gè)訂閱者可以共享信號(hào)值纽帖。
2宠漩、RACSubject訂閱處理邏輯
可以看到,訂閱前發(fā)送的信號(hào)訂閱者都不會(huì)收到懊直。
3扒吁、RACReplaySubject:是RACSubject子類,訂閱者在訂閱它之后會(huì)先將之前已經(jīng)發(fā)送的信號(hào)室囊,快速發(fā)送一遍給訂閱者雕崩。然后再回到當(dāng)前的現(xiàn)實(shí),等待下一個(gè)信號(hào)的到來融撞。
上面的博客中臧老師形象的用時(shí)空穿越的例子來描述:舉個(gè)生動(dòng)的例子盼铁,就好像科幻電影里面主人公穿越時(shí)間線后會(huì)先把所有的回憶快速閃過再來到現(xiàn)實(shí)一樣。(見《X戰(zhàn)警:逆轉(zhuǎn)未來》尝偎、《蝴蝶效應(yīng)》)所以我們也有理由認(rèn)定replaySubject天然也是熱信號(hào)饶火。
這一點(diǎn)不得不佩服,能把抽象的知識(shí)講的富有畫面感致扯,不得不說是只有對(duì)該領(lǐng)域有了充分且足夠深入的理解才能達(dá)到這種境界肤寝,佩服!
4抖僵、RACReplaySubject的訂閱時(shí)處理邏輯如下:
5醒陆、結(jié)論
RACSubject
及其子類
是熱信號(hào)。
RACSignal
排除RACSubject
類以外的是冷信號(hào)裆针。
四刨摩、冷信號(hào)轉(zhuǎn)熱信號(hào)
1、冷信號(hào)轉(zhuǎn)換成熱信號(hào)的本質(zhì)
冷信號(hào)轉(zhuǎn)換成熱信號(hào)的本質(zhì):就是使用一個(gè)subject訂閱原始信號(hào)世吨,讓其他訂閱者訂閱這個(gè)subject,這個(gè)subject就是熱信號(hào)澡刹。
2、代碼實(shí)現(xiàn):
- (void)coldSignalTransferHotSignal {
//1耘婚、創(chuàng)建冷信號(hào)
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,并訂閱冷信號(hào)
RACSubject *subject = [RACSubject subject];
NSLog(@"=======subject 被創(chuàng)建");
[[RACScheduler mainThreadScheduler] afterDelay:2.0 schedule:^{
[coldSignal subscribe:subject]; //放在主線程中訂閱
}];
//3沐祷、其他訂閱者訂閱subject
[subject subscribeNext:^(id x) {
NSLog(@"====第一個(gè)訂閱者嚷闭,收到信號(hào)值:%@",x);
}];
[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{
[subject subscribeNext:^(id x) {
NSLog(@"=====第二個(gè)訂閱者,收到信號(hào)值:%@",x);
}];
}];
}
3赖临、RAC官方給出的信號(hào)轉(zhuǎn)換API:
當(dāng)然胞锰,使用這種RACSubject來訂閱冷信號(hào)得到熱信號(hào)的方式仍有一些小的瑕疵。例如subject的訂閱者提前終止了訂閱兢榨,而subject并不能終止對(duì)coldSignal的訂閱嗅榕。
所以在RAC庫中對(duì)于冷信號(hào)轉(zhuǎn)化成熱信號(hào)有如下標(biāo)準(zhǔn)的封裝
- (RACMulticastConnection *)publish;
- (RACMulticastConnection *)multicast:(RACSubject *)subject;
- (RACSignal *)replay;
- (RACSignal *)replayLast;
- (RACSignal *)replayLazily;
這5個(gè)方法中顺饮,最為重要的就是- (RACMulticastConnection *)multicast:(RACSubject *)subject;
這個(gè)方法了,其他幾個(gè)方法也是間接調(diào)用它的凌那。至于multiCast的實(shí)現(xiàn)兼雄,可參閱博客,原文講的很好帽蝶。
4赦肋、使用multicast: 來完善冷熱信號(hào)轉(zhuǎn)換的本質(zhì):
- (void)multiCast {
//1、創(chuàng)建冷信號(hào)
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:將冷信號(hào)轉(zhuǎn)換成熱信號(hào)
RACSubject *subject = [RACSubject subject];
RACMulticastConnection *connection = [coldSignal multicast:subject];
/*
//1励稳、使用connect
RACSignal *hotSignal = connection.signal;
//主動(dòng)觸發(fā)connect
[[RACScheduler mainThreadScheduler] afterDelay:2.0 schedule:^{
[connection connect];
}];
*/
//2金砍、使用autoconnect
RACSignal *hotSignal = connection.autoconnect;
//訂閱熱信號(hào)
[hotSignal subscribeNext:^(id x) {
NSLog(@"====第一個(gè)訂閱者,收到信號(hào)值:%@",x);
}];
[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{ //4s后開始訂閱
[hotSignal subscribeNext:^(id x) {
NSLog(@"====第二個(gè)訂閱者麦锯,收到信號(hào)值:%@",x);
}];
}];
}
注意:看publish的源碼會(huì)發(fā)現(xiàn)恕稠,其實(shí)publish就是做了上面的工作。
5扶欣、replay鹅巍、replayLatest、replayLazily對(duì)比
- (RACSignal *)replay就是用RACReplaySubject來作為subject料祠,并立即執(zhí)行connect操作骆捧,返回connection.signal。其作用是上面提到的replay功能髓绽,即后來的訂閱者可以收到歷史值敛苇。
- (RACSignal *)replayLast就是用容量為1的RACReplaySubject來替換- (RACSignal *)replay的subject。其作用是使后來訂閱者只收到:
訂閱者訂閱前信號(hào)發(fā)送的最后一次歷史值顺呕。
- (RACSignal *)replayLazily和- (RACSignal *)replay的區(qū)別就是:
replayLazily只有在第一次訂閱的時(shí)候才訂閱sourceSignal枫攀。簡單講:直到訂閱的時(shí)候才真正創(chuàng)建一個(gè)信號(hào),源信號(hào)的訂閱代碼才開始執(zhí)行
具體看例子:
- (void)comparisonSignal {
//創(chuàng)建冷信號(hào)
RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"222 冷信號(hào)(原始信號(hào))被訂閱");
[[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)換成熱信號(hào)
// RACSignal *hotSignal = [coldSignal replayLazily];
RACSignal *hotSignal = [coldSignal replay];
NSLog(@"111開始訂閱");
//訂閱熱信號(hào)
[hotSignal subscribeNext:^(id x) {
NSLog(@"====第一個(gè)訂閱者株茶,收到信號(hào)值:%@",x);
}];
[[RACScheduler mainThreadScheduler] afterDelay:4 schedule:^{ //4s后開始訂閱
[hotSignal subscribeNext:^(id x) {
NSLog(@"====第二個(gè)訂閱者来涨,收到信號(hào)值:%@",x);
}];
}];
}
*********************************************************************
使用replay的輸出結(jié)果:
2017-12-19 16:22:00.338530+0800 222 冷信號(hào)(原始信號(hào))被訂閱
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 冷信號(hào)(原始信號(hào))被訂閱
我們可以看到,replayLazily 只會(huì)在訂閱時(shí)启盛,才會(huì)去創(chuàng)建信號(hào)蹦掐,源信號(hào)的訂閱代碼才會(huì)被執(zhí)行。
6僵闯、回到第二篇博客中的例子上卧抗,我們?yōu)榱吮苊饩W(wǎng)絡(luò)請(qǐng)求執(zhí)行多次,保證它只會(huì)執(zhí)行一次鳖粟,我們需要將冷信號(hào)轉(zhuǎn)換成熱信號(hào)(熱信號(hào)不會(huì)因?yàn)橛嗛喺叩挠嗛喩珩桑匦虏シ牛8膭?dòng)后的代碼如下:
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)換成熱信號(hào)牺弹,而且保證網(wǎng)絡(luò)請(qǐng)求的代碼是直到訂閱才去執(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會(huì)執(zhí)行多次时呀,將fetchData轉(zhuǎn)換為desc的block也會(huì)執(zhí)行多次张漂。但是由于這些block都是無副作用的晶默,計(jì)算量并不大,可以忽略不計(jì)航攒。如果計(jì)算量大的磺陡,也需要對(duì)中間的信號(hào)進(jìn)行熱信號(hào)的轉(zhuǎn)換。不過請(qǐng)不要忽略冷熱信號(hào)的轉(zhuǎn)換本身也是有計(jì)算代價(jià)的漠畜。
這里我們也可以總結(jié)下:當(dāng)模塊代碼會(huì)重復(fù)執(zhí)行多次時(shí)币他,我們想要避免這種情況,可以采取轉(zhuǎn)換成熱信號(hào)的方式憔狞。但是假如該代碼重復(fù)執(zhí)行不會(huì)產(chǎn)生副作用蝴悉,那么我們則可以允許這種情況的存在。