Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Remove from array of AnyCancellable when publisher finishes

Tags:

swift

combine

Is there a good way to handle an array of AnyCancellable to remove a stored AnyCancellable when it's finished/cancelled?

Say I have this

import Combine
import Foundation

class Foo {

    private var cancellables = [AnyCancellable]()

    func startSomeTask() -> Future<Void, Never> {
        Future<Void, Never> { promise in
            DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
                promise(.success(()))
            }
        }
    }

    func taskCaller() {
        startSomeTask()
            .sink { print("Do your stuff") }
            .store(in: &cancellables)
    }

}

Every time taskCaller is called, a AnyCancellable is created and stored in the array. I'd like to remove that instance from the array when it finishes in order to avoid memory waste.

I know I can do something like this, instead of the array

var taskCancellable: AnyCancellable?

And store the cancellable by doing:

taskCancellable = startSomeTask().sink { print("Do your stuff") }

But this will end to create several single cancellable and can pollute the code. I don't want a class like

class Bar {

    private var task1: AnyCancellable?
    private var task2: AnyCancellable?
    private var task3: AnyCancellable?
    private var task4: AnyCancellable?
    private var task5: AnyCancellable?
    private var task6: AnyCancellable?

}
like image 203
Rico Crescenzio Avatar asked Mar 14 '20 18:03

Rico Crescenzio


People also ask

When using combine's sink operator What happens if you don't store the AnyCancellable that gets returned?

Because without storing the AnyCancelable created by the sink, the garbage collector will delete the sink, I must store it.

How do I cancel AnyCancellable?

An AnyCancellable instance. Call cancel() on this instance when you no longer want the publisher to automatically assign the property. Deinitializing this instance will also cancel automatic assignment.

What is cancellable in Combine?

Cancellables in Combine fulfill an important part in Combine's subscription lifecycle. According to Apple, the Cancellable protocol is the following: A protocol indicating that an activity or action supports cancellation.


2 Answers

I asked myself the same question, while working on an app that generates a large amount of cancellables that end up stored in the same array. And for long-lived apps the array size can become huge.

Even if the memory footprint is small, those are still objects, which consume heap, which can lead to heap fragmentation in time.

The solution I found is to remove the cancellable when the publisher finishes:

func consumePublisher() {
    var cancellable: AnyCancellable!
    cancellable = makePublisher()
        .sink(receiveCompletion: { [weak self] _ in self?.cancellables.remove(cancellable) },
              receiveValue: { doSomeWork() })
    cancellable.store(in: &cancellables)
}

Indeed, the code is not that pretty, but at least there is no memory waste :)

Some high order functions can be used to make this pattern reusable in other places of the same class:

func cleanupCompletion<T>(_ cancellable: AnyCancellable) -> (Subscribers.Completion<T>) -> Void {
    return { [weak self] _ in self?.cancellables.remove(cancellable) }
}

func consumePublisher() {
    var cancellable: AnyCancellable!
    cancellable = makePublisher()
        .sink(receiveCompletion: cleanupCompletion(cancellable),
              receiveValue: { doSomeWork() })
    cancellable.store(in: &cancellables)
}

Or, if you need support to also do work on completion:

func cleanupCompletion<T>(_ cancellable: AnyCancellable) -> (Subscribers.Completion<T>) -> Void {
        return { [weak self] _ in self?.cancellables.remove(cancellable) }
    }
    
func cleanupCompletion<T>(_ cancellable: AnyCancellable, completionWorker: @escaping (Subscribers.Completion<T>) -> Void) -> (Subscribers.Completion<T>) -> Void {
    return { [weak self] in
        self?.cancellables.remove(cancellable)
       completionWorker($0)
    }
}

func consumePublisher() {
    var cancellable: AnyCancellable!
    cancellable = makePublisher()
        .sink(receiveCompletion: cleanupCompletion(cancellable) { doCompletionWork() },
              receiveValue: { doSomeWork() })
    cancellable.store(in: &cancellables)
}
like image 133
Cristik Avatar answered Oct 30 '22 20:10

Cristik


It's a nice idea, but there is really nothing to remove. When the completion (finish or cancel) comes down the pipeline, everything up the pipeline is unsubscribed in good order, all classes (the Subscription objects) are deallocated, and so on. So the only thing that is still meaningfully "alive" after your Future has emitted a value or failure is the Sink at the end of the pipeline, and it is tiny.

To see this, run this code

for _ in 1...100 {
    self.taskCaller()
}

and use Instruments to track your allocations. Sure enough, afterwards there are 100 AnyCancellable objects, for a grand total of 3KB. There are no Futures; none of the other objects malloced in startSomeTask still exist, and they are so tiny (48 bytes) that it wouldn't matter if they did.

like image 38
matt Avatar answered Oct 30 '22 19:10

matt