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 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订阅处理逻辑

ReactiveCocoa技术讲解-第四讲冷热信号和并发编程_第1张图片
1.png

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

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

ReactiveCocoa技术讲解-第四讲冷热信号和并发编程_第2张图片
3.png

5、结论

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

四、冷信号转热信号

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

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

2、代码实现:

- (void)coldSignalTransferHotSignal {
    //1、创建冷信号
    RACSignal *coldSignal = [RACSignal createSignal:^RACDisposable *(id 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 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 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 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都是无副作用的,计算量并不大,可以忽略不计。如果计算量大的,也需要对中间的信号进行热信号的转换。不过请不要忽略冷热信号的转换本身也是有计算代价的。

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

你可能感兴趣的:(ReactiveCocoa技术讲解-第四讲冷热信号和并发编程)