Swift Combine — Publisher和Subscriber的交互流程(自定义Publisher、Subscriber、Subscription)

之前的文章已经介绍过PublisherSubscriber,对于概念类的东西这里就不多介绍了,在介绍PublisherSubscriber的交互流程之前,先补充一下前面没有提到过的Subscription

Subscription

Subscription是一个协议,实现该协议的对象负责将订阅者链接到发布者。只要它在内存中,订阅者就会继续接收值。它只包含一个方法:

public protocol Subscription : Cancellable, CustomCombineIdentifierConvertible {/// Tells a publisher that it may send more values to the subscriber.func request(_ demand: Subscribers.Demand)
}

当订阅者在Publisher中接收到subscription对象后,便开始调用request方法,demand参数决定了订阅者要从发布者那里获取多少个值。

demand参数有几个可选的参数值:

  • none:表示订阅者一个值都不会收到。
  • max(value): 表示订阅者要接收value个值。
  • unlimited:表示订阅者要接受无限个值。

Subscription的实例对象中包含了一个Subscriber的引用,以使其保持最新状态。
Subscription协议有继承了Cancellable协议,所以有了cancel方法,而在自定义Subscription的时候,requestcancel方法都是必须实现的。

Publisher和Subscriber的交互流程

介绍完了Subscription协议,现在看看PublisherSubscriber是如何建立的联系。

  1. Publisher调用subscribe(_:) 方法开启链接申请,同时参数传入Subscriber实例对象。
  2. 在第一步调用subscribe(_:) 方法后,即触发Publisher内部调用receive(subscriber:)方法,在该方法中创建一个连接PublisherSubscriberSubscription对象,然后调用Subscriberreceive(subscription:)方法,将Subscription对象传给Subscriber
  3. Subscriberreceive(subscription:)方法中,使用传进来的subscription对象调用request方法,并设置Subscriber的请求次数。
  4. Subscriptionrequest方法中,知道了Subscriber的请求次数,经过相关的逻辑处理后,在此方法中给Subscriber发送数据。
  5. 通过Subscriberreceive(_:)方法向Subscriber发送数据。
  6. 通过Subscriberreceive(completion:)方法向Subscriber发送结束或者失败信息。

因为Subscription是起了一个桥梁的作用,属于幕后,所以上面第5条、第6条从语义上来说相当于Publisher通过receive(_:)方法或receive(completion:)方法向Subscriber发送数据或者结束信息。实际上SubscriptionPublisher做了向下游发送数据的事情。

自定义Subscriber

首先看一下Subscriber协议的定义:

public protocol Subscriber<Input, Failure> : CustomCombineIdentifierConvertible {associatedtype Inputassociatedtype Failure : Errorfunc receive(subscription: any Subscription)func receive(_ input: Self.Input) -> Subscribers.Demandfunc receive(completion: Subscribers.Completion<Self.Failure>)
}

协议中有两个类型,三个方法。自定义的Subscriber需要使用class定义,而非struct,否则会报错,另外struct是值类型,Subscription没有持有最初的那个Subscriber对象。

// 自定义Subscriber
class CustomSubscriber: Subscriber {// 确定输入类型,需要和Publisher的输出类型一致。typealias Input = Int// 确定失败类型,需要和Publisher的失败类型一致,永远不会失败就定义为Never。typealias Failure = Never/** 交互流程中第3步*  接收subscription对象的方法。*  方法内subscription对象调用request方法,设置请求次数。*/func receive(subscription: any Subscription) {debugPrint("CustomSubscriber subscription.request")subscription.request(.max(5))}/** 交互流程中第5步*  接收Publisher发送数据的方法。*  该方法返回`Subscribers.Demand`,用于在request方法中计算请求次数。*/func receive(_ input: Int) -> Subscribers.Demand {print("New value \(input)")return .none}/** 交互流程中第6步*  接收Publisher发送结束的方法,或者正常结束,或者失败。*/func receive(completion: Subscribers.Completion<Never>) {print("Completion: \(completion)")}
}

自定义Publisher

在自定义Publisher前,再看一下Publisher协议的定义:

public protocol Publisher<Output, Failure> {associatedtype Outputassociatedtype Failure : Errorfunc receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}

自定义的Publisher需要继承这个协议,比如:

// 自定义Publisher
class CustomPublisher: Publisher {// 确定输出类型,需要和Subscriber的输入类型一致。typealias Output = Int// 确定失败类型,需要和Subscriber的失败类型一致,永远不会失败就定义为Never。typealias Failure = Never/** 交互流程中第2步*  接收subscriber对象的方法。方法传入Subscriber实例对象,开始建立联系。*  方法内创建Subscription对象,然后调用Subscriber的receive(subscription:)方法,将Subscription对象传给Subscriber。*/func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {// 创建Subscription对象let subscription = CustomSubscription(subscriber: subscriber)debugPrint("CustomPublisher subscriber.receive")// 将Subscription对象传给Subscribersubscriber.receive(subscription: subscription)}
}

自定义Subscription

先看一下Subscription协议:

public protocol Subscription : Cancellable, CustomCombineIdentifierConvertible {/// Tells a publisher that it may send more values to the subscriber.func request(_ demand: Subscribers.Demand)
}

该协议中规定了要实现request方法,因为继承了Cancellable,所以还需要实现一个cancel方法。

public protocol Cancellable {func cancel()
}

Subscriber一样,自定义的Subscription需要使用class定义,而非struct,否则会报错,另外创建的Subscription实例对象需要在内存中保持,否则订阅就失效了。

下面是自定义Subscription

// 自定义Subscription
class CustomSubscription<S: Subscriber>: Subscription where S.Input == Int, S.Failure == Never {// 持有传入进来的Subscriber对象。private var subscriber: Sprivate var counter = 0private var isCompleted = false// 初始化的时候将Subscriber对象传入进来,并持有,待后续发送数据使用。init(subscriber: S) {self.subscriber = subscriber}/** 交互流程中第4步*  该方法传入请求数据的次数,并给Subscriber发送数据。*/func request(_ demand: Subscribers.Demand) {debugPrint("CustomSubscription request")guard !isCompleted else { return }for _ in 0..<(demand.max ?? 10) {_ = subscriber.receive(counter) // 给Subscriber发送数据counter += 1}if counter >= 5 {subscriber.receive(completion: .finished) // 通知Subscriber结束。isCompleted = true}}// 该方法中执行一些取消订阅的操作。func cancel() {isCompleted = true}
}

如何使用

定义完了上面的,现在看看怎么使用吧。还是依托SwiftUI的界面,我们在对应的ViewModel中添加方法,使用上面自定义的类。

首先定义一个ViewModel

class CustomCombineViewModel: ObservableObject {var subscription: AnyCancellable?func testMethod1() {// 创建自定义的Publisherlet publisher = CustomPublisher()// 创建自定义的Subscriberlet subscriber = CustomSubscriber()debugPrint("Begin subscribe")/** 交互流程中第1步,申请订阅。*  由Publisher对象调用subscribe方法,传入Subscriber对象开始。*/publisher.subscribe(subscriber)}func testMethod2() {// 创建自定义的Publisherlet publisher = CustomPublisher()// 通过sink方法申请订阅,并将创建的subscription持有,否则订阅失败,sink方法返回的时AnyCancellable,这里做了类型抹除。subscription = publisher.sink { completion inprint("sink completion: \(completion)")} receiveValue: { value inprint("sink new value \(value)")}}
}

在上面代码中的testMethod1方法中,分别创建了PublisherSubscriber,并用Publisher对象调用subscribe方法开启订阅,这也是订阅的开启入口。

当执行testMethod1时候,输出打印:

"Begin subscribe"
"CustomPublisher subscriber.receive"
"CustomSubscriber subscription.request"
"CustomSubscription request"
New value 0
New value 1
New value 2
New value 3
New value 4
Completion: finished

上面的输出也反应了从开始订阅到发送数据结束的过程。打印了5个数据是应为我们在Subscriber类中调用request方法的时候传入了.max(5),最多发送5个数据。

再看一下第二个方法testMethod2(),这个方法中没有明确的Publisher调用subscribe方法呢?

Subscribers有两个内置的Subscriber,分别为Subscribers.SinkSubscribers.Assign。当调用sink或者assign方法的时候,就开启了订阅流程。

当执行testMethod2时候,输出打印:

"CustomPublisher subscriber.receive"
"CustomSubscription request"
sink new value 0
sink new value 1
sink new value 2
sink new value 3
sink new value 4
sink new value 5
sink new value 6
sink new value 7
sink new value 8
sink new value 9
sink completion: finished

因为sink请求的是无限次数数据,所以将我们在Subscription中的数据都打印出来了。

Subscribers.Sink

Sink 创建的时候会立即调用 Subscription 对象的 request(.unlimited)

Publisher 有两个 sink 扩展方法:

  • sink(receiveCompletion:receiveValue:)
  • sink(receiveValue:)

Subscribers.Assign

Assign 会将接收到的值赋值给一个类对象的属性或者一个另一个 @Published publisher 上,它对 publisher 的 demand 也是 .unlimited

Publisher 有两个 assign 扩展方法:

  • assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root)
  • assign(to published: inout Published<Self.Output>.Publisher)

写在最后

现在我们完全理解了Combine订阅交互流程,是不是对Combine框架有了进一步的认识呢?
在实际开发过程中,不建议我们自己去实现PublisherSubscriberSubscription,因为一个逻辑错误可能会破坏发布者和订阅者之间的所有连接,这可能会导致意想不到的结果。

最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/36514.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

探索人工智能和LLM对未来就业的影响

近年来&#xff0c;人工智能&#xff08;AI&#xff09;迅猛发展&#xff0c;引发了人们的兴奋&#xff0c;同时也引发了人们对就业未来的担忧。大型语言模型&#xff08;LLM&#xff09;就是最新的例子。这些强大的人工智能子集经过大量文本数据的训练&#xff0c;以理解和生成…

Python 面试【初级】

欢迎莅临我的博客 &#x1f49d;&#x1f49d;&#x1f49d;&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

安宝特方案 | AR术者培养:AR眼镜如何帮助医生从“看”到“做”?

每一种新药品的上市都需要通过大量的临床试验&#xff0c;而每一种新的手术工具在普及使用之前也需要经过反复的实践和验证。医疗器械公司都面临着这样的挑战&#xff1a;如何促使保守谨慎的医生从仅仅观察新工具在手术中的应用&#xff0c;转变为在实际手术中实操这项工具。安…

011、MongoDB副本集数据同步机制深度解析

目录 MongoDB副本集数据同步机制深度解析 1. 副本集架构概述 1.1 基本组成 1.2 节点角色 2. 数据同步过程详解 2.1 初始同步 2.2 持续复制 2.3 Oplog详解 3. 数据一致性与可用性 3.1 写关注(Write Concern) 3.2 读偏好(Read Preference) 3.3 因果一致性会话 4. 高…

python教程--基础语法

python基础语法 2.1 缩进规则2.2 函数函数定义函数调用参数传递示例 2.3 类类的定义类的实例化类的属性和方法访问示例 2.4 顺序语句结构示例 2.5 条件和分支条件语句的基本结构示例单个 if 语句嵌套条件语句 2.6 循环For 循环While 循环循环控制语句 2.7 数据类型数字类型序列…

IBCS 虚拟专线——企业网络困境的破局者

企业对于高效、稳定且成本合理的网络解决方案的需求愈发迫切。作为一家企业的技术负责人&#xff0c;我曾深陷于企业网络的种种困境之中&#xff0c;直到 IBCS 虚拟专线的出现&#xff0c;为我们带来了转机。 我们的企业在发展过程中&#xff0c;面临着诸多网络相关的挑战。随…

第一百三十一节 Java面向对象设计 - Java对注释类型的限制

Java面向对象设计 - Java对注释类型的限制 注释类型的限制 注释类型不能从另一个注释类型继承。 每个注释类型都隐式地继承java.lang.annotation.Annotation接口&#xff0c;其声明如下&#xff1a; package java.lang.annotation;public interface Annotation { boolea…

FPGA开发技能(7)Vivado设置bit文件加密

文章目录 前言1. AES加密原理2.xilinx的AES方案3.加密流程3.1生成加密的bit流3.2将密钥写入eFUSE寄存器 4.验证结论5.传送门 前言 在FPGA的项目发布的时候需要考虑项目工程加密的问题&#xff0c;一方面防止自己的心血被盗&#xff0c;另一方面也保护公司资产&#xff0c;保护知…

少女之妙,妙在微笑

一、妙与不妙&#xff0c;少女与微笑 我们曾经解过汉字“妙”&#xff0c;妙字可以拆分为少女二字&#xff0c;即&#xff1a; 妙 女 少 少女 但这&#xff0c;其实并没有对 “妙”字 完成完整性解析&#xff0c;如果要完成完整性的说明&#xff0c;应当加上微笑&#xff0…

Windows系统将livp和HEIC文件批量转化为jpg

Windows系统将livp和HEIC文件批量转化为jpg&#xff01; 最重要的是 不用写代码&#xff01;不&#xff01;用&#xff01;写&#xff01;代&#xff01;码&#xff01; 不用写代码&#xff01;Window系统将livp和HEIC文件批量转化为jpg 免&#xff01;费&#xff01; 具体操作…

Linux平台下RTSP|RTMP播放器如何跟python交互投递RGB数据供视觉算法分析

技术背景 我们在对接Linux平台RTSP播放模块的时候&#xff0c;遇到这样的技术需求&#xff0c;开发者需要把Linux RTSP播放器拉取的数据&#xff0c;除了实时播放外&#xff0c;还要投递给python&#xff0c;用于视觉算法分析。 技术实现 Linux平台RTSP、RTMP直接播放不再赘…

IDM下载器怎么用 IDM下载器使用技巧 idm下载器怎么下载网页视频

IDM总能让新用户们眼前一亮&#xff0c;它不仅是工作学习上的好帮手&#xff0c;更是帮你解锁信息世界的钥匙。下载全程无广告、无弹窗、畅享高速下载&#xff0c;这无疑是下载软件市场中的一股清流。有关IDM下载器怎么用&#xff0c;IDM下载器使用技巧的问题&#xff0c;本文将…

App Inventor 2 列表排序,函数式编程轻松实现高级排序算法

本文主要介绍 列表 的高级用法&#xff0c;即函数式编程&#xff0c;可以按照指定的逻辑进行列表的排序&#xff0c;而无需我们自己写代码实现排序功能。 指定的逻辑也包括很复杂的逻辑&#xff0c;也就是说如果你的排序逻辑很复杂&#xff0c;函数式编程就是最好的使用场景。…

人工智能 (AI) 进阶【C#】版

使用C#和ML.NET进行图像分类任务。这个示例将展示如何加载图像数据、构建和训练模型&#xff0c;以及进行预测。 进阶版&#xff1a;图像分类 我们将使用ML.NET和预训练的TensorFlow模型进行图像分类。首先&#xff0c;确保你已经安装了以下NuGet包&#xff1a; dotnet add …

Vue--》从零开始打造交互体验一流的电商平台(四)完结篇

今天开始使用 vue3 + ts 搭建一个电商项目平台,因为文章会将项目的每处代码的书写都会讲解到,所以本项目会分成好几篇文章进行讲解,我会在最后一篇文章中会将项目代码开源到我的github上,大家可以自行去进行下载运行,希望本文章对有帮助的朋友们能多多关注本专栏,学习更多…

shiro漏洞利用记录

shiro漏洞利用记录 获取heapdump 访问http://39.100.119.172:8082/actuator/heapdump​下载heapdump nginx waf可能限制下载heapdump&#xff0c;但里面的配置可能是精确匹配&#xff0c;因此可以使用http://39.100.119.172:8082/actuator/heapdump//​来绕过 获取shrio ke…

Vue3使用Vue Router4實現頁面切換

前言 Vue Router 是 Vue 官方的客户端路由解决方案。 客户端路由的作用是在单页应用 (SPA) 中将浏览器的 URL 和用户看到的内容绑定起来。当用户在应用中浏览不同页面时&#xff0c;URL 会随之更新&#xff0c;但页面不需要从服务器重新加载。 下面我將開始介紹vue router的…

RuoYi-Vue3不启动后端服务如何登陆?

RuoYi-Vue3不启动后端服务如何登陆?RuoYi-Vue3使用的前端技术栈 是:Vue3 + Element Plus + Vite。 github开源地址:https://github.com/yangzongzhuan/RuoYi-Vue3 前后的分离在线演示项目地址:https://vue.ruoyi.vip/ 这种方式是用若依提供的在线后端接口,可以在此基础上修…

数据格式转换 | 稀疏矩阵3列还原为原始矩阵/数据框,自定义函数 df3toMatrix()

1. 输入3列 只要前三列&#xff0c;第一列是行名&#xff0c;第二列是列名&#xff0c;第三列为值。 > head(df.net2.order)from to strength type 12439 CSTF2 ENST0000056844 -0.6859788 neg 12015 CSTF2 ENST0000056190 -0.5153181 neg 11208 CSTF2 …

三阶魔方公式详解及快速解法方法介绍

三阶魔方公式详解及快速解法方法介绍 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们来深入探讨三阶魔方的公式及其快速解法方法。无论是初学者还是已经…