13-13.【Combine】share() 与 multicast() 的区别和底层原理?

5 阅读3分钟

在 Combine 中,share()multicast() 都属于共享操作符。它们存在的根本目的是解决**“重复订阅副作用”**的问题(例如:防止每增加一个订阅者就重新发起一次网络请求)。

虽然它们殊途同归,但由于底层对控制权时机的处理不同,适用场景大相径庭。


1. 核心区别:主动 vs 被动

特性.share().multicast()
控制权自动。第一个人订阅时启动,最后一个人离开时停止。手动。由开发者通过 .connect() 决定启动时机。
底层类型返回 Publishers.Share返回 ConnectablePublisher
状态持有不持有。新订阅者收不到过去的值。取决于传入的 Subject 类型。
使用复杂度极简,像“公共汽车”,上车就走。较高,像“发令枪”,准备好了才打火。

2. .share() 的底层原理:引用计数

.share() 实际上是一个快捷方式,它组合了 .multicast().autoconnect()

  • 引用计数逻辑:它内部维护一个计数器。

    • 第一个 Subscriber 订阅时,它会订阅上游并开始产生数据。
    • 后续 Subscriber 加入时,它只是将这些订阅者接入现有的流,不会重新触发上游。
    • 所有 Subscriber 都取消订阅时,它会自动断开与上游的连接。
  • 致命弱点:它不具备“记忆”。如果在上游发出值之后才有新订阅者加入,新来的人会错过之前的所有数据。


3. .multicast() 的底层原理:中继代理

.multicast() 的核心在于它引入了一个 Subject 作为中间代理。

  • 工作流程

    1. 你必须提供一个 Subject(如 PassthroughSubjectCurrentValueSubject)。
    2. 当你订阅 multicast 时,你实际上是在订阅这个 Subject
    3. 此时上游完全不会动作
    4. 只有当你显式调用 .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 或已经下载完的数据),这里有个巨大的坑:

  1. 第一个订阅者进来,上游瞬间发出值并完成(Finish)
  2. 引用计数回零。
  3. 第二个订阅者进来。
  4. 由于引用计数从 0 回到了 1,.share()再次去订阅上游。
  5. 副作用被二次触发了!

结论.share() 只在处理持续性的流(如传感器、定时器、尚未完成的网络请求)时才是安全的。如果处理的是“一闪而过”的同步数据,它可能无法实现你想要的共享效果。


总结

  • .share() 是为了省事,让流自动管理“按需广播”。
  • .multicast() 是为了精准控制,通过 Subject 实现“可定制的广播”。