Combine中向combineLatest发布者递归传值未触发死循环问题求解
Combine 非预期行为成因解析
核心原理:Combine 内置运算符的同步重入防护
Combine 所有系统内置运算符默认都是同步执行,且自带重入防护逻辑,combineLatest的内部运行规则符合这个设计:
- 内部会存储所有上游Publisher的最新值,仅当所有上游都至少发送过一次值后,才会向下游输出组合后的新值
- 当
combineLatest正处于向下游发送值的同步回调过程中时,如果此时收到任意上游的新值,只会更新自己内部存储的对应上游的最新值,不会立刻触发新的组合值输出,必须等当前这次输出的整个回调链路完全执行完毕后,才会检查是否存在需要下发的新组合值。
你的代码执行流程拆解
结合你给出的输出,可以逐步骤对应运行逻辑:
- 执行
pub1.send(1):pub1的handleEvents收到值打印Received new pub1 value: 1,但此时pub2还没有发送过任何值,combineLatest未满足输出条件,没有下游输出,流程暂时终止。 - 执行
pub2.send(2):combineLatest集齐两个上游的最新值(1,2),开始向下游输出:- 先经过第二个
handleEvents打印Received new combined value: (1, 2) - 进入sink回调,打印
(1, 2) - 同步执行
pub1.send(value.0)也就是pub1.send(1):- pub1的
handleEvents收到新值打印Received new pub1 value: 1 - 此时
combineLatest还处于上一次值的输出回调过程中,属于重入场景,只会把内部存储的pub1最新值更新为1(和之前存储的值完全一致)
- pub1的
- 先经过第二个
- sink回调执行完毕,
combineLatest检查内部存储的所有上游最新值,发现和上一次输出的组合值(1,2)完全相同,不会触发新的下游输出,整个流程终止,因此不会出现无限循环。
补充说明:如果你把sink里的发送逻辑改成
pub1.send(value.0 + 1),也就是每次发送和原有值不同的新值,你会看到每次sink回调结束后,都会触发新的组合值输出,形成无栈溢出的循环,只是不会同步递归调用而已。
其他关联现象解释
为什么移除combineLatest就会无限循环?
移除combineLatest之后,整条链路没有中间运算符的重入防护逻辑,你在sink回调里同步调用pub1.send时,会立刻触发下一轮的pub1输出,直接进入sink回调,形成无限递归调用,就会出现无限循环。
为什么加receive(on: DispatchQueue.main)就会触发循环?
receive(on: DispatchQueue.main)的作用是把后续的所有回调调度到主队列异步执行,这时候sink的回调已经脱离了combineLatest的同步输出上下文,combineLatest的重入防护不会触发。你在sink里调用pub1.send时,相当于在combineLatest的当前输出流程已经完全结束后,重新给上游发值,每次都会触发新的组合值输出,所以就会形成无限循环。
为什么merge(with:)会符合预期触发循环?
merge的内部逻辑比combineLatest简单很多:它不需要聚合多个上游的值,只要收到任意上游的新值,就会直接向下游透传,也没有combineLatest这种重入场景下的延迟输出逻辑,所以你在sink里同步给上游发值时,会立刻触发下一轮输出,形成循环。
内容的提问来源于stack exchange,提问作者Will Spurgeon
相关产品推荐
相关产品推荐

