Memory leak situation when storing a URLSession task in a property in Swift

222 views Asked by At

I'm trying to understand the memory leak situation in Swift language but there is a situation that I'm still wondering.

I've created a new UIViewController and call fetch function with storing the fetch task in a property without starting the task then I closed this UIViewController.

I found that the deinit function in this UIViewController is not called (Memory leak).

func fetchAPI() {
    let url = URL(string: "https://www.google.com")!
    let task = URLSession.shared.downloadTask(with: url) { _, _, _ in
        DispatchQueue.main.async {
            print(self.view.description)
        }
    }
    self.vcTask = task
}

But If I call the fetch function with calling resume method and then I close UIViewController again.

I found that the deinit function in this UIViewController is called (Memory not leak).

func fetchAPI() {
    let url = URL(string: "https://www.google.com")!
    let task = URLSession.shared.downloadTask(with: url) { _, _, _ in
        DispatchQueue.main.async {
            print(self.view.description)
        }
    }
    self.vcTask = task
    task.resume() // start downloading
}

For now I think that if I store a task in a property in UIViewController and I use self in the callback. It would create a cycle that caused Memory leak.

But when I call task.resume() Why the memory is not leak in this situation?

2

There are 2 answers

1
jrturton On BEST ANSWER

An un-resumed task will never execute its completion handler, because it will never complete. The task, and its handler, will therefore remain in memory.

We don't know the internal implementation of URLSession* but it would seem sensible for the framework to discard completion handlers once they are executed. This would break the retain cycle and allow the view controller to be deallocated.

You could confirm this by adding extra logging in the completion handler and deinit method - I would expect the view controller not to be deallocated until the completion handler has run.

1
Gereon On

(Adding to @jrturton's answer, which is 100% correct afaik)

This line of code

let task = URLSession.shared.downloadTask(with: url) { _, _, _ in ... }

captures self strongly, causing the memory leak.

One way to avoid this is to change the capture to be weak, like so:

let task = URLSession.shared.downloadTask(with: url) { [weak self] _, _, _ in
    guard let self else { return }
    DispatchQueue.main.async {
        print(self.view.description)
    }
}

Alternatively, try adding self.vcTask = nil to the ViewController's viewDidDisappear method to manually break the cycle.