写点什么

用于多播的 RACMulticastConnection

  • 2019 年 12 月 09 日
  • 本文字数:4742 字

    阅读完需:约 16 分钟

用于多播的 RACMulticastConnection

ReactiveCocoa 中的信号信号在默认情况下都是冷的,每次有新的订阅者订阅信号时都会执行信号创建时传入的 block;这意味着对于任意一个订阅者,所需要的数据都会重新计算,这在大多数情况下都是开发者想看到的情况,但是这在信号中的 block 有副作用或者较为昂贵时就会有很多问题。



我们希望有一种模型能够将冷信号转变成热信号,并在合适的时间触发,向所有的订阅者发送消息;而今天要介绍的 RACMulticastConnection 就是用于解决上述问题的。


RACMulticastConnection 简介

RACMulticastConnection 封装了将一个信号的订阅分享给多个订阅者的思想,它的每一个对象都持有两个 RACSignal



一个是私有的源信号 sourceSignal,另一个是用于广播的信号 signal,其实是一个 RACSubject 对象,不过对外只提供 RACSignal 接口,用于使用者通过 -subscribeNext: 等方法进行订阅。


RACMulticastConnection 的初始化

RACMulticastConnection 有一个非常简单的初始化方法 -initWithSourceSignal:subject:,不过这个初始化方法是私有的:


Objective-C


- (instancetype)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject {  self = [super init];
_sourceSignal = source; _serialDisposable = [[RACSerialDisposable alloc] init]; _signal = subject;
return self;}
复制代码


RACMulticastConnection 的头文件的注释中,对它的初始化有这样的说明:


Note that you shouldn’t create RACMulticastConnection manually. Instead use -publish or -multicast:.


我们不应该直接使用 -initWithSourceSignal:subject: 来初始化一个对象,我们应该通过 RACSignal 的实例方法初始化 RACMulticastConnection 实例。


Objective-C


- (RACMulticastConnection *)publish {  RACSubject *subject = [RACSubject subject];  RACMulticastConnection *connection = [self multicast:subject];  return connection;}
- (RACMulticastConnection *)multicast:(RACSubject *)subject { RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject]; return connection;}
复制代码


这两个方法 -publish-multicast: 都是对初始化方法的封装,并且都会返回一个 RACMulticastConnection 对象,传入的 sourceSignal 就是当前信号,subject 就是用于对外广播的 RACSubject 对象。


RACSignal 和 RACMulticastConnection

网络请求在客户端其实是一个非常昂贵的操作,也算是多级缓存中最慢的一级,在使用 ReactiveCocoa 处理业务需求中经常会遇到下面的情况:


Objective-C


RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {    NSLog(@"Send Request");    NSURL *url = [NSURL URLWithString:@"http://localhost:3000"];    AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];    NSString *URLString = [NSString stringWithFormat:@"/api/products/1"];    NSURLSessionDataTask *task = [manager GET:URLString parameters:nil progress:nil                                      success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {                                          [subscriber sendNext:responseObject];                                          [subscriber sendCompleted];                                      } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {                                          [subscriber sendError:error];                                      }];    return [RACDisposable disposableWithBlock:^{        [task cancel];    }];}];
[requestSignal subscribeNext:^(id _Nullable x) { NSLog(@"product: %@", x);}];
[requestSignal subscribeNext:^(id _Nullable x) { NSNumber *productId = [x objectForKey:@"id"]; NSLog(@"productId: %@", productId);}];
复制代码


通过订阅发出网络请求的信号经常会被多次订阅,以满足不同 UI 组件更新的需求,但是以上代码却有非常严重的问题。



每一次在 RACSignal 上执行 -subscribeNext: 以及类似方法时,都会发起一次新的网络请求,我们希望避免这种情况的发生。


为了解决上述问题,我们使用了 -publish 方法获得一个多播对象 RACMulticastConnection,更改后的代码如下:


Objective-C


RACMulticastConnection *connection = [[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {    NSLog(@"Send Request");    ...}] publish];
[connection.signal subscribeNext:^(id _Nullable x) { NSLog(@"product: %@", x);}];[connection.signal subscribeNext:^(id _Nullable x) { NSNumber *productId = [x objectForKey:@"id"]; NSLog(@"productId: %@", productId);}];
[connection connect];
复制代码


在这个例子中,我们使用 -publish 方法生成实例,订阅者不再订阅源信号,而是订阅 RACMulticastConnection 中的 RACSubject 热信号,最后通过 -connect 方法触发源信号中的任务。



对于热信号不了解的读者,可以阅读这篇文章 『可变』的热信号 RACSubject


publish 和 multicast 方法

我们再来看一下 -publish-multicast: 这两个方法的实现:


Objective-C


- (RACMulticastConnection *)publish {  RACSubject *subject = [RACSubject subject];  RACMulticastConnection *connection = [self multicast:subject];  return connection;}
- (RACMulticastConnection *)multicast:(RACSubject *)subject { RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject]; return connection;}
复制代码


-publish 方法调用时相当于向 -multicast: 传入了 RACSubject



-publish 只是对 -multicast: 方法的简单封装,它们都是通过 RACMulticastConnection 私有的初始化方法 -initWithSourceSignal:subject: 创建一个新的实例。


在使用 -multicast: 方法时,传入的信号其实就是用于广播的信号;这个信号必须是一个 RACSubject 本身或者它的子类:



传入 -multicast: 方法的一般都是 RACSubject 或者 RACReplaySubject 对象。


订阅源信号的时间点

订阅 connection.signal 中的数据流时,其实只是向多播对象中的热信号 RACSubject 持有的数组中加入订阅者,而这时刚刚创建的 RACSubject 中并没有任何的消息。



只有在调用 -connect 方法之后,RACSubject 才会订阅源信号 sourceSignal


Objective-C


- (RACDisposable *)connect {  self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal];  return self.serialDisposable;}
复制代码


这时源信号的 didSubscribe 代码块才会执行,向 RACSubject 推送消息,消息向下继续传递到 RACSubject 所有的订阅者中。



-connect 方法通过 -subscribe: 实际上建立了 RACSignalRACSubject 之间的连接,这种方式保证了 RACSignal 中的 didSubscribe 代码块只执行了一次。


所有的订阅者不再订阅原信号,而是订阅 RACMulticastConnection 持有的热信号 RACSubject,实现对冷信号的一对多传播。


RACMulticastConnection 中还有另一个用于连接 RACSignalRACSubject 信号的 -autoconnect 方法:


Objective-C


- (RACSignal *)autoconnect {  __block volatile int32_t subscriberCount = 0;  return [RACSignal    createSignal:^(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]; } }]; }];}
复制代码


它保证了在 -autoconnect 方法返回的对象被第一次订阅时,就会建立源信号与热信号之间的连接。


使用 RACReplaySubject 订阅源信号

虽然使用 -publish 方法已经能够解决大部分问题了,但是在 -connect 方法调用之后才订阅的订阅者并不能收到消息。


如何才能保存 didSubscribe 执行过程中发送的消息,并在 -connect 调用之后也可以收到消息?这时,我们就要使用 -multicast: 方法和 RACReplaySubject 来完成这个需求了。


Objective-C


RACSignal *sourceSignal = [RACSignal createSignal:...];RACMulticastConnection *connection = [sourceSignal multicast:[RACReplaySubject subject]];[connection.signal subscribeNext:^(id  _Nullable x) {    NSLog(@"product: %@", x);}];[connection connect];[connection.signal subscribeNext:^(id  _Nullable x) {    NSNumber *productId = [x objectForKey:@"id"];    NSLog(@"productId: %@", productId);}];
复制代码


除了使用上述的代码,也有一个更简单的方式创建包含 RACReplaySubject 对象的 RACMulticastConnection


Objective-C


RACSignal *signal = [[RACSignal createSignal:...] replay];[signal subscribeNext:^(id  _Nullable x) {    NSLog(@"product: %@", x);}];[signal subscribeNext:^(id  _Nullable x) {    NSNumber *productId = [x objectForKey:@"id"];    NSLog(@"productId: %@", productId);}];
复制代码


-replay 方法和 -publish 差不多,只是内部封装的热信号不同,并在方法调用时就连接原信号:


Objective-C


- (RACSignal *)replay {  RACReplaySubject *subject = [RACReplaySubject subject];  RACMulticastConnection *connection = [self multicast:subject];  [connection connect];  return connection.signal;}
复制代码


除了 -replay 方法,RACSignal 中还定义了与 RACMulticastConnection 中相关的其它 -replay 方法:


Objective-C


- (RACSignal<ValueType> *)replay;- (RACSignal<ValueType> *)replayLast;- (RACSignal<ValueType> *)replayLazily;
复制代码


三个方法都会在 RACMulticastConnection 初始化时传入一个 RACReplaySubject 对象,不过却有一点细微的差别:



相比于 -replay 方法,-replayLast 方法生成的 RACMulticastConnection 中热信号的容量为 1


Objective-C


- (RACSignal *)replayLast {  RACReplaySubject *subject = [RACReplaySubject replaySubjectWithCapacity:1];  RACMulticastConnection *connection = [self multicast:subject];  [connection connect];  return connection.signal;}
复制代码


replayLazily 会在返回的信号被第一次订阅时,才会执行 -connect 方法:


Objective-C


- (RACSignal *)replayLazily {  RACMulticastConnection *connection = [self multicast:[RACReplaySubject subject]];  return [RACSignal    defer:^{      [connection connect];      return connection.signal;    }];}
复制代码


总结

RACMulticastConnection 在处理冷热信号相互转换时非常好用,在 RACSignal 中也提供了很多将原有的冷信号通过 RACMulticastConnection 转换成热信号的方法。



在遇到冷信号中的行为有副作用后者非常昂贵时,我们就可以使用这些方法将单播变成多播,提高执行效率,减少副作用。


References


Github Repo:iOS-Source-Code-Analyze


Source: https://draveness.me/racconnection


本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/racconnection


2019 年 12 月 09 日 15:56283

评论

发布
暂无评论
发现更多内容

如何在 GitHub 上面为你的项目选择开源许可证

HoneyMoose

如何提升工作效率

wangwei1237

工作效率 文化 大历史理论

从零搭建一款PC页面编辑器PC-Dooring

徐小夕

大前端 可视化 lowcode 代码编辑器

超硬核!阿里技术大牛肝了2晚整理的Java知识,这也太强了!

飞飞JAva

Java Java泛型

通向未来的十二个趋势

CECBC

人工智能

网络攻防学习笔记 Day2

穿过生命散发芬芳

5月日更 网络攻防

微服务-技术专题-微服务进程间通信

浩宇の天尚

微服务 分布式架构 5月日更

限时白嫖!腾讯内部员工培训Java资料,网友:大厂就是不一样

牛哄哄的java大师

Java

OAuth 2.0 了解了,OAuth 2.1 呢?

Zhang

OAuth 2.0 认证授权 OAuth 2.1

Redis-技术专题-Redis分布式锁实现方案

浩宇の天尚

redis 分布式锁 5月日更

C++基础语法

IT蜗壳-Tango

【LeetCode】砖墙Java题解

Albert

算法 LeetCode 5月日更

复杂Gremlin查询的调试方法

Tom(⊙o⊙)

gremlin调试

Redis-技术专区-知识问题总结大全(上篇)

浩宇の天尚

redis 5月日更 问题分析

数字化转型能力成为中国纺织服装业未来发展的核心动能

CECBC

纺织面料

【音视频】弱网下的音视频通讯

Bob

音视频 直播技术

当你觉得老板的决策是错的,你会怎么做?

石云升

职场经验 5月日更

多家银行增设数字金融部 架构调整背后透露出哪些信号?

CECBC

银行

未来5年或将出现颠覆型区块链应用,资产通证化将重构实体经济

CECBC

区块链

【人间碎片】关于努力这件事

南吕

人生修炼 人生故事

高级研发工程师都有哪些特点?【超级准】

liuzhen007

技术人生 工作体会 程序猿

算法训练营 - 学习笔记 - 第四周

心在飞

名可名

顿晓

5月日更 命名

我与 InfoQ 写作平台的这些事

xcbeyond

个人成长 1 周年盛典 InfoQ 写作平台 1 周年 5月日更

顺序一致性(Sequential Consistency)

UNDEFINED

sequential consistency Java Concurrency distributed system

模块三作业

c

架构实战营

3.5 Go语言从入门到精通:标准输入输出fmt包

xcbeyond

Go 语言 5月日更 fmt包

如何选择开源许可证

HoneyMoose

SwiftUI @ Netflix:推动新技术落地是怎样一种体验?

故胤道长

swift 移动开发 iOS Developer SwiftUI

区块链如何推动人力资源和薪酬管理体系变革?

CECBC

人力资源

【JS】作用域(入门篇)

德育处主任

JavaScript 大前端 Web js

用于多播的 RACMulticastConnection_语言 & 开发_Draveness_InfoQ精选文章