ReactiveCocoa技术讲解-第四讲冷热信号和并发编程

一、冷热信号:

美团冷热信号1
1、热信号是主动的,即使你没有订阅事件,它仍然会时刻推送。
而冷信号是被动的,只有当你订阅的时候,它才会发送消息。

2、热信号可以有多个订阅者,是一对多,信号可以与订阅者共享信息。
而冷信号只能一对一,当有不同的订阅者,消息会重新完整发送。

二、为什么要区分冷、热信号:

美团冷热信号2
这里面引用的例子很说服力,足见臧老师的功底之深。我决定把例子在这里详细的讲解下:

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中实际运行一遍,不要只是单纯的看代码。切身体会下这段的问题所在。

分析前先下结论:

1、信号只有被订阅后才会产生值。
2、任何信号变换的本质都是依赖bind函数,而bind函数的实现在上一篇中我们已经讲过,所以这里直接有的概念就是:任何信号的转换都是对原有信号进行订阅,从而产生新信号。
3、这里一定要注意的是:我们在最外层创建信号后,在内部对原始信号进行订阅时,用到的subscriber是组外层信号的订阅者,也就是只有新创建的信号被订阅时,我们内部才会间接地对原始信号进行订阅。

有了这些前置概念,我们再来看下上面的代码:

1、fetchData信号被flattenMap之后,会因为title、desc被订阅从而间接的被订阅,desc被flattenMap后生成renderedDesc,等到renderedDesc被订阅后,fetchData会再次被间接订阅,因此会有三次订阅的过程,也就是会产生三次网络请求。
2、我们看到上述代码还有一个merge操作,这里会将三个信号merge成为一个新信号,创建了一个新的信号,在这个信号被订阅的时候,把它包含的所有信号订阅。所以我们又得到了额外的3次网络请求。

总结:每一次的订阅都会导致信号被重新执行,从而引起6次网络请求,而造成这种现象的原因是:fetchData是一个冷信号。所以每次订阅都会重新执行一次。如果是热信号,即使被订阅多次,我们也不会,因为每次订阅,信号都会被执行一次。

这篇博客中也提到了:FP(函数式编程)以及副作用的相关概念,这里不在细说,大家可以自行阅读。

三、如何处理冷、热信号

美团冷热信号3

RACSubject 和RACReplaySubject :

1、RACSubject:
1.1、RACSubject是热信号,他的订阅者在订阅后,不会收到在订阅前发送的信号值,只会收到从订阅时间点开始后产生的信号值。
1.2、多个订阅者可以共享信号值。
2、RACSubject订阅处理逻辑

1.png

可以看到,订阅前发送的信号订阅者都不会收到。

3、RACReplaySubject:是RACSubject子类,订阅者在订阅它之后会先将之前已经发送的信号,快速发送一遍给订阅者。然后再回到当前的现实,等待下一个信号的到来。
上面的博客中臧老师形象的用时空穿越的例子来描述:举个生动的例子,就好像科幻电影里面主人公穿越时间线后会先把所有的回忆快速闪过再来到现实一样。(见《X战警:逆转未来》、《蝴蝶效应》)所以我们也有理由认定replaySubject天然也是热信号。这一点不得不佩服,能把抽象的知识讲的富有画面感,不得不说是只有对该领域有了充分且足够深入的理解才能达到这种境界,佩服!
4、RACReplaySubject的订阅时处理逻辑如下

3.png

5、结论

RACSubject及其子类是热信号。
RACSignal排除RACSubject类以外的是冷信号。

四、冷信号转热信号

1、冷信号转换成热信号的本质

冷信号转换成热信号的本质:就是使用一个subject订阅原始信号,让其他订阅者订阅这个subject,这个subject就是热信号。

2、代码实现:

- (void)coldSignalTransferHotSignal {
    //1、创建冷信号
    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、创建subject,并订阅冷信号
    RACSubject *subject = [RACSubject subject];
    NSLog(@"=======subject 被创建");
    [[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官方给出的信号转换API:
当然,使用这种RACSubject来订阅冷信号得到热信号的方式仍有一些小的瑕疵。例如subject的订阅者提前终止了订阅,而subject并不能终止对coldSignal的订阅。所以在RAC库中对于冷信号转化成热信号有如下标准的封装

- (RACMulticastConnection *)publish;
- (RACMulticastConnection *)multicast:(RACSubject *)subject;
- (RACSignal *)replay;
- (RACSignal *)replayLast;
- (RACSignal *)replayLazily;

这5个方法中,最为重要的就是- (RACMulticastConnection *)multicast:(RACSubject *)subject;这个方法了,其他几个方法也是间接调用它的。至于multiCast的实现,可参阅博客,原文讲的很好。
4、使用multicast: 来完善冷热信号转换的本质:

- (void)multiCast {
    //1、创建冷信号
    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:将冷信号转换成热信号
    RACSubject *subject = [RACSubject subject];
    RACMulticastConnection *connection = [coldSignal multicast:subject];
    
    /*
    //1、使用connect
    RACSignal *hotSignal = connection.signal;
    //主动触发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的源码会发现,其实publish就是做了上面的工作。

5、replay、replayLatest、replayLazily对比

  • (RACSignal *)replay就是用RACReplaySubject来作为subject,并立即执行connect操作,返回connection.signal。其作用是上面提到的replay功能,即后来的订阅者可以收到历史值。
  • (RACSignal *)replayLast就是用容量为1的RACReplaySubject来替换- (RACSignal *)replay的subject。其作用是使后来订阅者只收到:订阅者订阅前信号发送的最后一次历史值。
  • (RACSignal *)replayLazily和- (RACSignal *)replay的区别就是:replayLazily只有在第一次订阅的时候才订阅sourceSignal。简单讲:直到订阅的时候才真正创建一个信号,源信号的订阅代码才开始执行
    具体看例子:
- (void)comparisonSignal {
    //创建冷信号
    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;
    }];
    //分别使用以下两种方式转换成热信号
//    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的输出结果:
2017-12-19 16:22:00.338530+0800  222 冷信号(原始信号)被订阅
2017-12-19 16:22:00.338857+0800 111开始订阅

使用replayLazily的输出结果:
2017-12-19 16:23:23.577210+0800  111开始订阅
2017-12-19 16:23:23.577920+0800  222 冷信号(原始信号)被订阅

我们可以看到,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];  // 使用replayLazily 转换成热信号,而且保证网络请求的代码是直到订阅才去执行。

    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都是无副作用的,计算量并不大,可以忽略不计。如果计算量大的,也需要对中间的信号进行热信号的转换。不过请不要忽略冷热信号的转换本身也是有计算代价的。

这里我们也可以总结下:当模块代码会重复执行多次时,我们想要避免这种情况,可以采取转换成热信号的方式。但是假如该代码重复执行不会产生副作用,那么我们则可以允许这种情况的存在。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容