在 Combine 中,share() 和 multicast() 都属于共享操作符。它们存在的根本目的是解决**“重复订阅副作用”**的问题(例如:防止每增加一个订阅者就重新发起一次网络请求)。
虽然它们殊途同归,但由于底层对控制权和时机的处理不同,适用场景大相径庭。
1. 核心区别:主动 vs 被动
| 特性 | .share() | .multicast() |
|---|---|---|
| 控制权 | 自动。第一个人订阅时启动,最后一个人离开时停止。 | 手动。由开发者通过 .connect() 决定启动时机。 |
| 底层类型 | 返回 Publishers.Share | 返回 ConnectablePublisher |
| 状态持有 | 不持有。新订阅者收不到过去的值。 | 取决于传入的 Subject 类型。 |
| 使用复杂度 | 极简,像“公共汽车”,上车就走。 | 较高,像“发令枪”,准备好了才打火。 |
2. .share() 的底层原理:引用计数
.share() 实际上是一个快捷方式,它组合了 .multicast() 和 .autoconnect()。
-
引用计数逻辑:它内部维护一个计数器。
- 当 第一个 Subscriber 订阅时,它会订阅上游并开始产生数据。
- 当 后续 Subscriber 加入时,它只是将这些订阅者接入现有的流,不会重新触发上游。
- 当 所有 Subscriber 都取消订阅时,它会自动断开与上游的连接。
-
致命弱点:它不具备“记忆”。如果在上游发出值之后才有新订阅者加入,新来的人会错过之前的所有数据。
3. .multicast() 的底层原理:中继代理
.multicast() 的核心在于它引入了一个 Subject 作为中间代理。
-
工作流程:
- 你必须提供一个
Subject(如PassthroughSubject或CurrentValueSubject)。 - 当你订阅
multicast时,你实际上是在订阅这个Subject。 - 此时上游完全不会动作。
- 只有当你显式调用
.connect()时,multicast才会去订阅上游,并将上游的值通过Subject广播给所有人。
- 你必须提供一个
-
强大之处:你可以控制“发车时间”。例如,你希望等 3 个不同的模块都准备好订阅后,再整齐划一地发起网络请求。
4. 典型应用场景
场景 A:UI 状态共享(选 .share)
一个网络请求返回用户头像,界面上有两个地方要显示。
- 做法:
requestPublisher.share().receive(on: Main).assign(...) - 结果:只发一次请求,两个 UI 组件同时更新。
场景 B:复杂初始化同步(选 .multicast)
App 启动时需要下载基础配置。由于初始化顺序不确定,A、B、C 三个模块可能先后订阅这个配置流。
- 做法:使用
multicast(subject: CurrentValueSubject(...))。 - 结果:即使 A 先订阅了,流也不会跑。等 C 订阅完,调用
connect(),大家一起拿数据。且CurrentValueSubject还能保证 D 晚一点进来也能拿到缓存的配置。
5. 防御式陷阱:.share() 的生命周期
如果你对 .share() 的上游是一个即时完成的 Publisher(如 Just 或已经下载完的数据),这里有个巨大的坑:
- 第一个订阅者进来,上游瞬间发出值并完成(Finish) 。
- 引用计数回零。
- 第二个订阅者进来。
- 由于引用计数从 0 回到了 1,
.share()会再次去订阅上游。 - 副作用被二次触发了!
结论:
.share()只在处理持续性的流(如传感器、定时器、尚未完成的网络请求)时才是安全的。如果处理的是“一闪而过”的同步数据,它可能无法实现你想要的共享效果。
总结
.share()是为了省事,让流自动管理“按需广播”。.multicast()是为了精准控制,通过Subject实现“可定制的广播”。