3

I have class for download files:

class FileDownloader {

    private let downloadsSession = URLSession(configuration: .default)
    private var task: URLSessionDownloadTask?
    private let url: URL

    init(url: URL) {
        self.url = url
    }

    public func startDownload(){
        download()
    }

    private func download(){

        task = downloadsSession.downloadTask(with: url) {[weak self] (location, response, error) in
            guard let weakSelf = self else {
                assertionFailure("self was deallocated")
                return }
            weakSelf.saveDownload(sourceUrl: weakSelf.url, location: location, response: response, error: error)
        }

        task!.resume()
    }

    private func saveDownload(sourceUrl : URL, location : URL?, response : URLResponse?, error : Error?) {
        if error != nil {
            assertionFailure("error \(String(describing: error?.localizedDescription))")
            return }

        let destinationURL = localFilePath(for: sourceUrl)

        let fileManager = FileManager.default
        try? fileManager.removeItem(at: destinationURL)
        do {
            try fileManager.copyItem(at: location!, to: destinationURL)
            print("save was completed at \(destinationURL) from \(String(describing: location))")
        } catch let error {
            print("Could not copy file to disk: \(error.localizedDescription)")
        }
    }

    private func localFilePath(for url: URL) -> URL {
        let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        return documentsPath.appendingPathComponent(url.lastPathComponent)
    }
}

When i call startDownload() i get an error at debugging at line:

assertionFailure("self was deallocated")

When i change download function to this:

private func download(){

        task = downloadsSession.downloadTask(with: url) {(location, response, error) in
            self.saveDownload(sourceUrl: self.url, location: location, response: response, error: error)
        }

        task!.resume()
    }

All work nice, but i'm afraid it may cause problems with object that is not properly released in memory. How to avoid such situation? Am i doing things right?

Evgeniy Kleban
  • 6,794
  • 13
  • 54
  • 107

2 Answers2

3

First, why are you getting that assertion failure? Because you’re letting FileDownloader instance fall out of scope. You haven’t shared how you’re invoking this, but you’re likely using it as a local variable. If you fix that, your problem goes away.

Second, when you changed your implementation to remove the [weak self] pattern, you don’t have a strong reference cycle, but rather you’ve just instructed it to not release the FileDownloader until the download is done. If that’s the behavior you want, then that’s fine. It’s a perfectly acceptable pattern to say “have this keep a reference to itself until the asynchronous task is done.” In fact, that’s precisely what URLSessionTask does. Clearly, you need to be absolutely clear regarding the implications of omitting the [weak self] pattern, as in some cases it can introduce a strong reference cycle, but not in this case.


Strong reference cycles only occur when you have two objects with persistent strong references to each other (or sometimes more than two objects can be involved). In the case of URLSession, when the download is done, Apple prudently wrote downloadTask method so that it explicitly releases the closure after calling it, resolving any potential strong reference cycle.

For example, consider this example:

class Foo {
    func performAfterFiveSeconds(block: @escaping () -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
            self.doSomething()

            block()
        }
    }

    func doSomething() { ... }
}

The above is fine because asyncAfter releases the closure when it’s run. But consider this example where we save the closure in our own ivar:

class BarBad {
    private var handler: (() -> Void)?

    func performAfterFiveSeconds(block: @escaping () -> Void) {
        handler = block

        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
            self.calledWhenDone()
        }
    }

    func calledWhenDone() {
        // do some stuff

        doSomething()

        // when done, call handler

        handler?()
    }

    func doSomething() { ... }
}

Now this is a potential problem, because this time we save the closure in an ivar, creating a strong reference to the closure, and introducing risk of a classic strong reference cycle.

But fortunately this is easily remedied:

class BarGood {
    private var handler: (() -> Void)?

    func performAfterFiveSeconds(block: @escaping () -> Void) {
        handler = block

        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
            self.calledWhenDone()
        }
    }

    func calledWhenDone() {
        // do some stuff

        doSomething()

        // when done, call handler

        handler?()

        // make sure to release handler when done with it to prevent strong reference cycle

        handler = nil
    }

    func doSomething() { ... }
}

This resolves the strong reference cycle when it sets handler to nil. This is effectively what URLSession (and GCD methods like async or asyncAfter) do. They save the closure until they call it, and then they release it.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • for example i used it in unit testing like that: func testDownload() { let downloader = FileDownloader(url: URL(string: URLS.firstFileUrl.rawValue)!) downloader.startDownload() waitForExpectations(timeout: largeTimeout, handler: nil) } – Evgeniy Kleban Jan 30 '19 at 00:21
  • in which case it does make strong reference retain cycle i should be worry about? – Evgeniy Kleban Jan 30 '19 at 14:10
  • @EvgeniyKleban - You need to worry about strong reference cycles when you have objects that have persistent keep strong references to each other. In this case, `downloadTask` releases the closure after calling it, resolving any cycle. Where you need to worry about it is where we know it won’t get released (e.g. a repeating timer that never stops; we have our own closure ivar and neglect to set it to `nil` when we’re done with it; we have strong delegate reference; etc.). – Rob Jan 30 '19 at 17:51
  • If you have concerns about strong reference cycles, you can run your actual app, exercise it, return to quiescent state where the relevant objects should be released, and click on the “debug memory graph” button https://stackoverflow.com/questions/30992338/how-to-debug-memory-leaks-when-leaks-instrument-does-not-show-them/30993476#30993476 – Rob Jan 30 '19 at 17:51
  • By the way, here are some unrelated observations on your code snippet: https://gist.github.com/robertmryan/fd52d2dcc4cdbb8632d1bf59f598a342 – Rob Jan 30 '19 at 18:07
1

Instead of use this:

task = downloadsSession.downloadTask(with: url) {(location, response, error) in
            self.saveDownload(sourceUrl: self.url, location: location, response: response, error: error)
        }

move it to delegates of URLSessionDownloadTask and URLSession

class FileDownloader:URLSessionTaskDelegate, URLSessionDownloadDelegate

and implement its methods:

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        if totalBytesExpectedToWrite > 0 {
            let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
            debugPrint("Progress \(downloadTask) \(progress)")
        }
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        debugPrint("Download finished: \(location)")
        try? FileManager.default.removeItem(at: location)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        debugPrint("Task completed: \(task), error: \(error)")
    }

I know that this value won't be nil but try to avoid forcing unwrapping:

task!.resume()

Download tasks directly write the server’s response data to a temporary file, providing your app with progress updates as data arrives from the server. When you use download tasks in background sessions, these downloads continue even when your app is suspended or is otherwise not running.

You can pause (cancel) download tasks and resume them later (assuming the server supports doing so). You can also resume downloads that failed because of network connectivity problems.

Camilo Lopez
  • 166
  • 5