2020 Google开发者大会重磅开幕 了解详情

深入浅出Apple响应式框架Combine

2020 年 11 月 12 日

深入浅出Apple响应式框架Combine

一、引言


Combine.framework 是 Apple 在 2019 WWDC 上基于 Swift 推出的函数响应框架(Functional Reactive Programming),支持 Apple 全平台的操作系统(iOS13+,macOS 10.15+等)。函数式响应框架无论在哪个平台早已流行泛滥,开源的Rx更是实现了各种语言的响应式编程框架。Apple 在这个时候推出响应式框架,无疑是对自己护城河的进一步巩固。事实上 SwiftUI 的数据驱动就是依赖 Combine。


本文将深入浅出地介绍 Combine 的基本概念和原理,然后通过具体 demo 详细阐述其在实际编码中的应用。


二、什么是 Combine


Combine is Swift declarative framework for processing values over time


Combine 为处理随着时间变化的值的一种声明式框架。Combine 作用是将异步事件通过组合事件处理操作符进行自定义处理。关注如何处理变化的值,正是响应式编程的核心。


Combine 可以概述为一种声明式的函数响应式编程,简洁用下图表示:



通过此图,我们可以总结 Combine 是什么:


Combine = Publishers + Operators + Subscribers


2.1 Publishers


Publisher sends sequences of values over time to one or more Subscribers.


发布者(Publisher)随着时间变化发送一系列的值给一个或者多个订阅者(Subscriber)。



一个发布者可以发布一个 value,Value 的类型为 Output,有两种状态:成功/失败。成功会发送 Value,失败就会产生 Failure,Failure 的类型为 Error。当然如果一个发布者永远不失败,那么失败就是 Never 类型。


Combine 内置的 Publisher 有 Just, Future, Deferred, Empty, Fail, Record, Published 以及 PassthroughSubject 和 CurrentValueSubject。Published 实际上是用 propertyWrapper 封装的 Publisher,它可以将任意一个变量封装成一个 Publisher,并通过 projectedValue(影子变量)轻松实现 MVVM,本文将在后续介绍。


2.2 Subscribers


Subscriber receives values from a publisher.


订阅者(Subscriber)接收发布者发送的 Value。订阅者遵循的协议如下:



订阅者接受一个 Input 类型的 Value 以及接收到事件失败的类型 Failure。protocol 中的三个 receive 方法描述了订阅三种不同的生命周期,本文会在后续 2.5 介绍。


Publisher 发布者协议中有两个通用类型参数 Output 和 Failure。而 Subscriber 订阅者接受一个 Input 类型的 Value 或者接收到事件已经发送成功或者失败。既然订阅者和发布者都有了,接下来的关键是如何连接他们,连接他们的是 Subscribtion(订阅),我们将在 2.5 中详细介绍。


使用 sink 方法和 assign 方法将在 Combine 内部自动创建 subcribtion 连接发布者和订阅者。Publisher 发布者协议中有两个通用类型参数 Output 和 Failure,而 Subscriber 订阅者接收发布者产生的 Output 和 Failure,因为发布者和订阅者是互相协作的,所以一个匹配的发布者和订阅者会有 Output==Input 和 Failure == Failure,如果不匹配,编译器会自动报错提示我们。


Combine 内置了两种 Subscribers,分别是 Subscribers.Sink 和 Subscriber.Assign。简单举例说明:



注释 1 中我们创建了 List,并使用内置的 Publishers.Sequence<[Int], Never>创建了 Publisher,其中 Int 是输入参数 Output,明显是数组中的单个元素,并且指定了失败类型为 Never。然后我们创了 subscriber,指定 input 为 Int,Failure 为 Never。然后通过 subscriber 方法连接他们,subcriber 方法会在内部创建 subcription 连接 Publisher 和 Subscriber。最终输出如下:


receiveValue:1 receiveValue:2 receiveValue:3 receiveValue:4 receiveValue:5 receiveValue:6 finished
复制代码


得益于 Swift 的 Extension,我们可以将上述创建的 subscriber 包裹到 Publisher 的 Extension 中,所以就有了注释 2 的简化版。进一步,我们可以拓展序列的 Extension,将 publisher 封装到 Sequence 的扩展中,所以才有最终简化版方法注释 3。


2.3 Subject


Subject 主题是一种特殊的发布者,它可以自己主动传送 Value 到 Combine 事件流中,接口如下:



Combine 内置了两种 Subject,分别是 PassthrougSubject 和 CurrentValueSubject,他们的含义都很明确。这里我们通过举例来说明 PassthrougSubject:



上述代码中我们创建 subject 对象,指定 Output 为 String,Failure 为 ExampleError。然后通过 sink 产生订阅者连接,sink 方法返回的是 Anycancellabel 对象,它表示一个发布者和订阅者的链接可取消,通过 store 方法将其保存在外部变量 setList 数组中,这样能保证订阅者不会被释放。最终输出结果如下:


Subscriber received value: Hello! Subscriber received value: Hello Again! failure(CombineTest2.ExampleError.somethingWentWrong)
复制代码


从输出中可以看到一旦一个事件流完成(completion)或者遇到 Error 后,后续再发送其他的值,由于此时事件流已经结束,所以输出结果中不会再有后续的 send 的 Value。


2.4 Operators


响应式编程的核心其实是 Publishers 各种转换,为什么要有操作符?因为任何一个事件流中,往往最初的对象和我们最后产生的对象并不一致,这时候就要通过 Operator 来转换输入的对象。Combine 中的 Operator 是将一个 Publisher 作为输入对象,通过 operator 产生另一个 Publisher。


Combine 中的各种 operators 是定义在 Publisher 的各种 Extension 中。在各自的扩展中实现了内置的 classes 或者 structures。举例来说,map(:)操作符返回的对象是 Publishers.Map 对象。Apple 目前内置了 50 多种 Operators,尽管这样,它仍然比 Rxswift 少,这里有一份 Combine 和 RxSwift 的操作符对比RxSwift to Combine Cheatsheet。我们列举几种简单的如下图:



我们通过 URLSession 内置的 dataPublisher 发送网络请求解析来说明用法,目的是为了说明 Combine 中的异步 API 以及在异步 API 中如何使用 Operator。代码示例如下图:



1)我们定义了常见的网络请求的错误类型;


2)UserResponse 返回的是服务端的 json 数据 Model;


3)判断 URL 是否有误,如果异常,返回 PassthroughSubject 生成的订阅者,发送 unsupportUrl 的 Failure 告知外部事件流结束;


4)tryMap 的 Input 类型是 dataPublisher 返回的元组(data: Data, response: URLResponse),我们判断 http 的 statusCode 是否异常,如果异常直接 thorw 错误,否则将元组的第一个元素 data 返回,所以对应的 Output 为 Data,Failure 为 CustomAPIError;


5)通过 decode 操作符将 data 转换为 UserResponse,decode 的失败 Failure 类型为 Error;


6)处理 tryMap 和 decode 产生的 Error,将其全部转换为 CustomAPIError;


7)最后通过 earseToAnyPublisher 将内部产生的 Publisher 类型擦除,因为外部关心的是 Publisher 携带的 UserResponse 和 CustomAPIError;


8)最终调用 sink 方法可以轻松的接送服务返回的数据。


2.5 生命周期


在 2.2 中我们已经说明了连接 Publisher/Subscriber 实际是通过一个中介对象 Subscription。完成的流程如下图:



  • 1-3,当一个订阅者Subscriber创建和绑定到发布者Publisher,订阅者Publisher将创建一个Subscription对象,并将subscriber的引用发给Subscription,这是时序图中的步骤1-3。

  • 4,这时候订阅者Subscriber和发布者Publisher已经通过subscription绑定了,订阅者Subscriber就可以通过request(_ demand:)方法获取它想要多少个Value,demand参数实际上是一个Int包裹值,类型包括:


Subscribers.Demand.none Subscribers.Demand.unlimited Subscribers.Demand.max(Int)
复制代码


当我们通过 Publisher 的 sink 方法创建 subscription 时候,实际 request 的参数是.unlimited,因为它想接收发布者 Publisher 发送的所有 values。大多情况这是我们想要的,但是某些情况如果想要限制订阅者的请求次数,那么就可以通过定义具体的 max 携带的 Int 值,比如:



上图中我们自定义了 IntSubscriber,在 receive(subscription:)方法中最多请求接收 2 次 Value,所以在 console 会输出如下:


Received subscription Received input:0 Received input:1
复制代码


最终我们只接收到 2 次 Input,由此可见 Demand 决定了订阅者和 Subscription 的生命周期。


  • 5,收到订阅者Subscriber的请求后,subscription通过发布者Publisher发送Value给订阅者Subscriber;

  • 6-7,subscription根据Demand的值来提交value,通过调用receive(_:)方法发送value,直到到达发送次数Demand的最大值;

  • 8,订阅者接收subscription发送的value,作为响应,它将返回一个新的Demand,注意到demand会根据先前已发送的demand进行相加,所以demand会保持至少不会减少;

  • 一旦新的Demand被subscription接收到,subscription又会根据demand重新来提交value,因此整个6-8过程是循环过程,一直到接收到completion或者Failure整个事件流才完全结束,这就是时序图中的9-10。


2.6 Debugging


响应式编程的最大痛点就是出现 bug 难以排查问题,但 Apple 设计的 API 通常简洁好用且方便调试。Apple 提供了 print()和 HandleEvents()两种 API 来控制输出,方便开发者调试。


通过举例来说明:



我们将数组[1,2,3,4]的 publisher 过滤数组中的奇数,然后通过 map 将转换成平方,在此通过 map 转换成 String,最终终端输出如下:


receive subscription: ([1, 2, 3, 4]) //1 request unlimited //2 receive value: (1) //3 request max: (1) (synchronous) //4 receive value: (2) //5 receiveValue=EventNumber=4 //6 receive value: (3) request max: (1) (synchronous) receive value: (4) receiveValue=EventNumber=16 receive finished completion=finished
复制代码


注释 1 是我们通过数组最终转换的 Publisher.Map 通过 receive 方法连接订阅者 AnySubscriber,然后创建 subscription 连接他们,紧接着 subscriber 通过 request(:)方法获取需要知道请求多少个 value,这里是无限次。


然后 subscription 提交 value,subscriber 通过 receive(:)方法接收 value:1,并返回响应 Demand.max(1)。这里涉及到 Filter 的实现问题,由于 1 不是偶数,因为不满足我们的条件,在 Combine 的 Publishers.Filter 中会在 receive(_:)方法中将不满足过滤条件的 value 返回 max(1),从而保证事件流下一次执行。注释 5 接收到 value:2,满足 fliter 然后进行 map 转换提交 value,一直到事件 complete 完成,整个事件流才结束。这里佐证了我们在 2.5 时序图描述是正确的。


上述 print()函数也可以替换成 HandleEvents(),他们没有太大的区别,但是能给我们提供更好的输入以及提供手动设置断点。



输出如下:


receiveRequest:unlimited receiveSubscription:[1, 2, 3, 4] receiveOutput:1 receiveRequest:max(1) receiveOutput:2 receiveOutput:3 receiveRequest:max(1) receiveOutput:4 receiveCompletion:finished
复制代码


此外还有 breakpointOnError()和 breakpoint(),本文限于长度不在累述,详情可参考 Apple 官方文档。


三、实战


3.1 自定义 Publishers 和 Subscribers


iOS13 系统内置了诸如 KVO,Notification,URLSession,Timer 的 Publisher,所以大部分场景开发者不需要自定义的 Publisher,但某些场景也有特定需求。UIKit 本身自带了很多 UI 控件的事件,但 iOS 系统本身确没有给出内置的 publisher,为此我们需要自定义 UIControl 的 Publisher。


3.1.1 自定义 Subscription


在创建 Plublisher 之前,我们先创建 Subscrption,因为 Subscription 是连接发布者和订阅者的中介者,没有它 Combine 事件流无法驱动。



我们定义了 UIControlSubscription:


1)构造器带有三个参数:分别是 subscriber,control,以及 Control 的事件类型。我们保存 subcriber,是因为在接下来的点击事件中,要让 subcriber 接收 Value。因为点击事件不会有失败类型,所以限制 Failure 类型为 Never;


2)实现 cancel 方法,以便于 Combine 能正确的释放资源;


3)注意到 Subscription 在初始化时候回调用 receive(:)方法,系统内置的 Subscriber.Sink 在 receive 方法中会调用 request(.unlimited),这里不再 request 填充任何代码,是因为我们只想当点击事件发生就立即处理,无论当前的请求次数是多少。一旦用户出发了点击事件,就会执行 eventHandler 方法,订阅者 subcriber 就会立即接收我们 UIControl。


3.1.2 自定义 Publisher


当 UIControl 的事件发生时,需要将 UIControl 本身传递出去。我们定义 UIControlPublisher 代码如下:



1)UIControlPublisher 的 Output 传递为 UIControl 本身,Failure 为 Never。


2)在构造器中,除了传递 UIControl,还将事件 UIControl.Event 传递进去,因为我们想要处理不同的 UI 事件。


3)receive 方法是将订阅者连接到发布者上,我们内部创建在 3.1.1 中定义的 UIControlSubscription,然后调用 subscriber 的 receive 方法向 Publisher 请求接收 Control。


3.1.3 使用 UIControlPublisher


我们在 UIControlPublisher 中使用了泛型,这样在拓展 UIControl 子控件时候就可以无需转换,方便地直接使用。拓展常见的 UI 事件的 Publisher 如下:



1)使用 Extension 集成了 publisher 的通用方法,这样它的所有子控件都可以快速使用该方法。


2)除了通用的 publisher,我们还使用 Extension 扩展了 UITextFiled 输入框监听文字可变的 Publisher,以及 UISwitch 开关状态的 Publisher。


3.1.4 补充说明


我们自定义了 UIControlPublisher,限于篇幅不会再定义其他诸如异步事件处理的 Publisher。虽然 Combine 本身是闭源的,但 Combine 在 Foundation 层的代码确是开源的,有兴趣的读者可以参考 Swift 源码中Publishers+URLSession.swiftPublishers+NotificationCenter.swift进一步了解 Publisher 和 Subscriber 是如何协同工作的原理。


3.2 实战


实现一个简单登录注册的 UI,如下所示:



界面很简单,就是用户名,密码,确认密码三个输入框以及同意隐私协议开关按钮和注册按钮。来给定一个简单的验证规则:


1)当用户输入登录名称大于等于 6 位;


2)密码和确认密码相等并且至少为 6 位;


3)用户同意隐私协议;


同时满足上述三个条件时注册按钮才点击可用,我们使用 Combine 来实现注册校验逻辑。


3.2.1 ViewModel


用 Combine 来实现 MVVM,首先显示 RegisterViewModel,如下:



1)注意到我们使用了 Property Wrapper 的 Published 来生成一个 Publisher,Published 包装任意一个变量成为 Publisher,并且可以使用 $符号表示其自身实际的 Publisher。分别定义用户名 username,密码 password,二次确认密码 confirmPwd,以及同意协议 accept 的 Publisher。


2)定义 validToRegisterPublisher 为注册按钮可点击的 Publisher 为只读属性,内部使用 CombineLatest 操作符来生成新的 Publisher,CombineLatest 会依赖发布者产生的最新 value 值,然后通过 map 转换我们要求的验证规则是否合法,返回 Bool 类型,true 表示注册按钮可点击。最后在使用 eraseToAnyPublisher()来擦除产生的发布者类型,因为使用者最终只关心发布者携带的 value 值的类型。


3.2.2 Bind ViewModel


下面是在 VC 中具体 bind ViewModel 的示例:



1)我们实现了 bindView()方法,该方法将 UI 控件通过在 3.1 中封装的自定义 UIControlPublisher 实现 UI 控件的事件绑定,并且将 Publisher 产生的值绑定到 ViewModel 中对应的 Publisher 中。


2)调用系统内置的 assign 方法将 validToRegisterPublisher 产生的 value 绑定到按钮的 isEnabled 属性上。


最终我们用 Combine 实现了 MVVM 模式的注册业务。


四、性能表现


RxSwift 已在开源社区广泛应用,Apple 本身推出的 Combine 的性能表现如何呢?我们使用Will Combine kill RxSwift?的测试代码来比较 Combine 和 RxSwift,代码如下:


class CombineVSRxSwiftTests: XCTestCase {
private let input = stride(from: 0, to: 1_000_000, by: 1)
override class var defaultPerformanceMetrics: [XCTPerformanceMetric] {
return [ XCTPerformanceMetric("com.apple.XCTPerformanceMetric_TransientHeapAllocationsKilobytes"),
.wallClockTime
]
}

func testCombine() {
self.measure {
_ = Publishers.Sequence(sequence: input)
.map { $0 * 2 }
.filter { $0.isMultiple(of: 2) }
.flatMap { Just($0) }
.count()
.sink(receiveValue: {
print($0)
})
}
}

func testRxSwift() {
self.measure {
_ = Observable.from(input)
.map { $0 * 2 }
.filter { $0.isMultiple(of: 2) }
.flatMap { Observable.just($0) }
.toArray()
.map { $0.count }
.subscribe(onSuccess: { print($0) })
}
}
}
复制代码


在 2019 iMac 16G 上测试出 Time 和 Allocation 的情况如下:




可以看到 Combine 的性能惊人,比 RxSwift 好的不止一个等级。当然 Combine 本身是闭源的,我们猜测 Apple 工程师可能使用大量的 C/C++代码来优化性能,而 RxSwift 则是纯 Swift 实现,性能表现则不是那么特别优异。


五、总结


本文系统的介绍了 Combine 的 Publisher,Subscriber,以及 Operator 的工作原理,并在了解工作原理的基础上自定义了 UIControlPublisher,然后结合实际案例介绍了如何使用 Combine 实现 MVVM 模式,最后我们比较了 Combine 和目前流行的 RxSwift,显而易见 Combine 在性能上有巨大的优势。


Combine 只支持 iOS13,或许让部分开发者觉得实际离我们还很远,但截止目前,根据 Apple 在 2020WWDC 前公布的数据,iOS13 设备占有率已达 92%以上,相信随着 iOS14 的到来,iOS13 占有率会更高,提前了解和掌握 Combine 还是很有必要的。


作者介绍


青花瓷的平方,携程技术专家,主要从事无线开发,负责携程支付 iOS 相关开发工作。


本文转载自公众号携程技术(ID:ctriptech)。


原文链接


深入浅出Apple响应式框架Combine


2020 年 11 月 12 日 14:00 604

评论

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

测试开发工程师修炼手册—测试技能大盘点

Zoe

测试工程师产出

架构师训练营第4周总结

aoeiuvzcs

系统架构师week04 Homework - 互联网架构技术手段和方案

尔东雨田

极客大学架构师训练营

猿灯塔:疫情冲击,去体验远程面试被怼10分钟,今年Java开发找工作真难

猿灯塔

Go: 字符串和转换优化

陈思敏捷

go golang string 字符串

原创 | TDD工具集:JUnit、AssertJ和Mockito (二十五)运行测试-在IDE中运行测试

编程道与术

Java intellij-idea 编程 TDD 单元测试

java架构-一些设计上的基本常识

猿灯塔

Java

听说你还没学Spring就被源码编译劝退了?30+张图带你玩转Spring编译

程序员DMZ

spring Spring源码编译

Kafka 消息丢失与消费精确一次性

古月木易

kafka

向女朋友解释乐观锁与悲观锁的小妙招!

小闫

spring 面试 Spring Cloud 乐观锁 悲观锁

Redis分布式锁课堂开课了!

小闫

redis Spring Cloud Redis项目

ARTS Week6

时之虫

ARTS 打卡计划

忘掉 Snowflake,感受一下性能高出 587 倍的全局唯一 ID 生成算法

今日长剑在握

golang redis 架构 分布式 CAP

五分钟让你搞懂Nginx负载均衡原理及四种负载均衡算法

架构大数据双料架构师

架构师训练营 第五周 作业

Poplar

理解 Mysql 索引底层原理只需这一篇就够了

小谈

MySQL 数据结构 Spring Cloud Spring Boot Java 面试

高承实:区块链在新基建中的作用和未来发展

CECBC区块链专委会

新基建 政策扶持 技术特征 链上数据 产业场景

为什么我建议你读一读历史?

Phoenix

历史 中国历史

信创舆情一线--印度封禁59款中国App

统小信uos

App 舆情 印度

AndroidStudio真机调试 - Waiting for Debugger

麦叔

Android Studio 真机调试

分布式柔性事务之最大努力通知事务详解

奈学教育

分布式事务

阿里技术官:这样带你学Spring全家桶,其实没你想的那么难

小吴选手

spring Spring Cloud Spring Boot

太牛 了!快码住!GitHub上标星75k!超牛的《Java面试突击版》

犬来八荒

Java git Linux 面试 Java 面试

逆袭之路,普通二本的八年开发码农如何进阿里拿年薪百万

小谈

Java 面试

自动特征工程在推荐系统中的研究

天枢数智运营

人工智能 推荐系统

一致性hash

彭阿三

一致性hash

Kafka 消息丢失与消费精确一次性

奈学教育

kafka

马匹、马镫、马车,和华为的数据基础设施革新

脑极体

游戏夜读 | 互动剧的黎明到了?

game1night

程序员的晚餐 | 7 月 3 日 好久没做饭

清远

美食

分布式柔性事务之最大努力通知事务详解

古月木易

分布式事务

2020中国技术力量年度榜单盛典

2020中国技术力量年度榜单盛典

深入浅出Apple响应式框架Combine-InfoQ