Swift — Combine Subscribers

讓我們一起來研讀 Combine 中的 Subscribers 吧!

Jeremy Xue
Jeremy Xue ‘s Blog
7 min readAug 2, 2020

--

讀完了發布者 Publishers 之後,接著讓我們看看重要的訂閱者 Subscribers 吧!而訂閱者在 Combine 中也是一個重要的角色,負責接收從 Publishers 所發出的值以及完成。

摘要:

  • Subscriber
  • Subscribers
  • AnySubscriber
  • Subscription
  • Subscriptions
  • 使用訂閱者處理已發布元素

✒︎ Subscriber

宣告一個可以接收來自發布者輸入類型的協議。

訂閱者實例從發布者接收元素流,以及描述其關係變動的生命週期事件。給定訂閱者的 InputFailure 關聯類型必須與相應發布者的 OutputFailure 相匹配。

透過調用發布者的 subscribe(_:) 方法連接訂閱者到發布者。進行此調用後,發布者將調用訂閱者的 receive(subscription:) 方法。這給予訂閱者一個 Subscription 實例,該實例用於向發布者要求元素,並有選擇的取消訂閱。訂閱者提供初始需求時,發布者可能會異步調用 receive(_:) 來傳遞新發布的元素。如果發布者停止發布,它將使用類型 Subscribers.Completion 的參數調用 receive(completion:) 來表示發布是正常完成還是出錯。

Combine 為 Publisher 類型提供以下訂閱者作為運算符:

  • sink(receiveCompletion:receiveValue:) 在接收到完成信號以及每次接收到新元素時執行閉包。
  • assign(to:on:) 將每個新接收到的值寫入給定實例 key path 上標識的屬性。

✒︎ Subscribers

作為訂閱者的類型的名稱空間。

基本上與上一篇文章中的 Publishers 的用途大同小異,一樣提供一個命名空間。

✒︎ AnySubscriber

一個類型擦除的訂閱者

使用 AnySubscriber 來包裝你不想暴露其詳細細節的現有訂閱者。妳還可以使用 AnySubscriber 透過對 Subscriber 中定義的方法的提供閉包來創建自定義訂閱者。而不是直接實現 Subscriber

✒︎ Subscription

表示訂閱者與發布者連結的協議。

訂閱受到類的約束,因為 Subscription 具有標誌,標識就是由特定的訂閱者連接到發布者的時後確定的。取消必須 Subscription 是線程安全的。

你只能取消一次 Subscription

取消訂閱將釋放先前透過連接 Subscriber 分配的任何資源。

✒︎ 使用訂閱者處理已發布元素

應用背壓(back pressure)來精準控制發布者何時產生元素。

在 Combine 中,Publisher 產生元素,而 Subscriber 對接收的元素進行操作。然而,在訂閱者連接並要求元素之前,發布者無法發送元素。訂閱者還透過使用 Subscribers.Demand 類型來表示發布者可以接受多少個元素來控制發布者傳遞元素的比率。訂閱者可以以兩種方式表示要求:

  • 透過在發布者首次訂閱時提供的 Subscription 實例上調用 request(_:)
  • 透過在發布者調用訂閱者的 receive(_:) 方法來傳遞元素時返回的新需求。

需求是可添加的:如果訂閱者要求兩個元素,然後請求 Subscribers.Demand(.max(3)),則發布者未滿足的需求現在為五個元素。如果發布者隨後發布元素,則未滿足的需求將減少到四個。發布元素是減少未滿足需求的唯一方式。訂閱者不能請求負需求。

許多應用僅使用運算符 sink(receiveValue:) 以及 assign(to:on:) 分別創建便捷訂閱者類型 Subscribers.SinkSubscribers.Assign。這兩個訂閱者在首次連接到發布者時發出無限制的要求。一但發布者的需求不受限制,訂閱者和發布者之間就無法進一步的談判需求。

在發布者產生元素時消耗它們

當發布者需求很高或不受限制時,它發送元素的速度可能比訂閱者處理元素的速度還快。這種狀況下可能導致元素被丟棄,或在元素等待處理時填滿緩衝區時,迅速增加存儲壓力。

如果你使用便捷訂閱者,則可能會發生這種狀況,因為它們需要無限數量的元素。確保你提供給 sink(receiveValue:) 以及 assign(to:on:) 的閉包的副作用遵循以下特徵:

  • 不要阻止發布者。
  • 不要透過緩衝元素來消耗過多的存儲。
  • 不要不知所措並且失敗的處理元素。

幸運的,許多常見的發布者,例如與用戶界面元素相關聯的發布者,以可以管理的比率發布。其他常見的發布者僅會生成一個元素,例如 URL 加載系統的 URLSession.DataTaskPublisher。與這些發布者一起使用 sinkassign 訂閱者是絕對安全的。

使用自定義訂閱者應用背壓

要控制發布者向你的訂閱者發送元素的比率,請創建 Subscriber 協議的自定義實現。使用你的實現來指定你知道訂閱者可以跟上的需求。當訂閱者接收元素時,它可以透過返回新的需求值到 receive(_:) 或透過在訂閱上調用 request(_:) 來請求更多內容。無論你使用哪種方式,你的訂閱者都可以在任何給定時間內微調發布者可以發送的元素數量。

透過發送信號通知訂閱者準備接收元素來控制流量的概念稱之為背壓(back pressure)。

每個發布者都保持追蹤其當前未滿足的需求,這意味著一個訂閱者已經要求了多少元素。甚至像是 Foundation 的 Timer.TimerPublisher 之類的自動化來源,只有在有待定需求時才產生元素。下面的程式碼說明了這個行為。

訂閱者的 receive(subscription:) 實現在向發布者請求任何元素之前使用五秒鐘的延遲。在這段期間,發布者存在並且具有有效的訂閱者,但需求為零,因此部會產生任何元素。它只會在延遲到期之後才開始發布元素,並且訂閱者將其需求設置為非零的值,如以下輸出所示:

此範例只要求三個元素,在五秒鐘後的延遲到期時發出需求。結果,發布者在第三個元素之後將不再發送其他元素,但也不會透過發送 finished 值來完成發布,因為發布者只是在等待更多需求。為了繼續接收元素,訂閱者可以存儲訂閱以及定期請求更多元素。它還可以將新需求指示為來自其 receive(_:) 實現的返回值。

透過背壓運算符管理無限制需求

即使沒有編寫自定義訂閱者,你仍然可以使用 Combine 的緩衝或時間運算符來應用背壓。

  • buffer(size:prefetch:whenFull:):持有來自上游發布者的固定數量項目。當飽滿時,緩衝區會丟棄元素或拋出錯誤。
  • debounce(for:scheduler:options:):只有在上游發布者在指定的時間間隔內停止發布時才發布。
  • throttle(for:scheduler:latest:):以給定的最大比率產生元素。如果在一段時間間隔內收到多個元素,則它只會發送最新或是最早的元素。
  • collect(_:) & collect(_:options:):綑綁元素直到它們超過給定的數量或時間區隔,向你發送元素陣列。如果你的使用者可以同時處理多個元素,那麼這種方式很好。

由於這些運算符控制訂閱者接收的元素數量,因此可以連接要求無限元素的訂閱者,像是 sink(receiveValue:)assign(to:on:)

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]