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?
}
Because without storing the AnyCancelable created by the sink, the garbage collector will delete the sink, I must store it.
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.
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.
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)
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With