Having memory leak issue while asynchronously iterating over AsyncPublisher (kind of async sequence)!
In the following code I have timerSequence (AsyncPublisher<Publishers.Autoconnect<Timer.TimerPublisher>>
) and on inti I’m asynchronously iterating over that timer sequence. But event after [weak self]
capture inside Task
it’s still not deallocating! Wondering it’s a Combine’s bug!!
GitHub demo: https://github.com/satishVekariya/AnyPublisherMemoryLeakDemoApp
import Foundation
import Combine
class MyServiceClass {
let timerSequence = Timer.publish(every: 1, on: .main, in: .default).autoconnect().values
init() {
Task { [weak self] in
await self?.setUpStreamIteration()
}
}
func setUpStreamIteration() async {
for await time in timerSequence {
print(time)
}
}
}
var service: MyServiceClass? = MyServiceClass()
service = nil
Output:
2023-03-26 00:14:11 +0000
2023-03-26 00:14:12 +0000
2023-03-26 00:14:13 +0000
2023-03-26 00:14:14 +0000
2023-03-26 00:14:15 +0000
2023-03-26 00:14:16 +0000
2023-03-26 00:14:17 +0000
2023-03-26 00:14:18 +0000
2023-03-26 00:14:19 +0000
...
2
Answers
First, I’m going to suggest that we simplify the example:
Once
setUpStreamIteration
starts running, theMyServiceClass
cannot be released untilsetUpStreamIteration
finishes. But in the absence of something to stop this asynchronous sequence, this method will never finish. Inserting aweak
capture list inTask {…}
will not save you oncesetUpStreamIteration
starts. (It actually introduces a race between the deallocation of the service and the starting of this method, which complicates our attempts to reproduce/diagnose this problem.)One approach, if you are using SwiftUI, is to create the stream in a
.task
view modifier, and it will automatically cancel it when the view is dismissed. (Note, for this to work, one must remain within structured concurrency and avoidTask { … }
unstructured concurrency.)The other typical solution is to explicitly opt into unstructured concurrency, save the
Task
when you start the sequence and add a method to stop the sequence. E.g.:And you just need to make sure that you call
stopStreamIteration
before you release it (e.g., when the associated view disappears).By the way, if you want to avoid introducing Combine, you can use
AsyncTimerSequence
from the Swift Async Algorithms package.You still have the same issue about needing to cancel this sequence (using one of the two approaches outlined above), but at least it avoids introducing Combine into the mix). You could also write your own
AsyncStream
wrappingTimer
or a GCD timer, but there’s no reason to reinvent the wheel.using .sink I managed to prevent the leak like so:
but to be honest, I can’t say for sure why it managed to work this way.
so even if it works for you, I wouldn’t sign my answer as the correct without someone else adding a clear explanation