2

I have an application that (in part) displays the current download speed of a Wi-Fi connection to a user. It does so by opening a URLSession and downloading a moderate-sized (~10MB) file and measuring the time it took.

Here's that URLSession function:

func testSpeed() {

    Globals.shared.dlStartTime = Date()
    Globals.shared.DownComplete = false

    if Globals.shared.currentSSID == "" {
        Globals.shared.bandwidth = 0
        Globals.shared.DownComplete = true
    } else {

        let url = URL(string: [HIDDEN])
        let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
        let task = session.downloadTask(with: url!)

        task.resume()
    }
}

public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {


    Globals.shared.dlFileSize = (Double(totalBytesExpectedToWrite) * 8) / 1000
    let progress = (Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100.0

    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ProcessUpdating"), object: nil, userInfo: ["progress" : progress])
}

public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    let elapsed = Double( Date().timeIntervalSince(Globals.shared.dlStartTime))
    Globals.shared.bandwidth = Int(Globals.shared.dlFileSize / elapsed)
    Globals.shared.DownComplete = true
    Globals.shared.dataUse! += (Globals.shared.dlFileSize! / 8000)
    session.invalidateAndCancel()

    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ProcessFinished"), object: nil, userInfo: nil)
}

As you can probably tell by the delegate functions, this all exists in a separate class from the view controller, along with some other small networking functions, like getting IP and SSID. The delegates post notifications that are observed by the view controller.

My ViewController has an NSTimer that calls back to this URLSession every 5 seconds to re-test the speed (but only runs it if the previous one completed). Here's the code for that:

reloadTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(rescanNetwork), userInfo: nil, repeats: true)

Which calls this function:

func backgroundRescan() {
    if Globals.shared.DownComplete {
        Networking().testSpeed()
    }
}

Which runs the URL Session again, checking, of course, to ensure that the previous had completed.

For some reason, I'm getting a massive buildup of memory use in testing, until the app reaches 2GB of memory use and gets terminated with the console output Message from debugger: Terminated due to memory issue. This all happens within two minutes of running the app.

I even added session.invalidateAndCancel() to the completion delegate in a desperate attempt to clear that memory. But it didn't work. Am I missing something?

Tom Crews
  • 67
  • 9
  • Are you sure the Globals.shared.currentSSID is being set to something other than "" ? If not, you will add a process every 5 seconds until you blow up. – Mozahler Mar 28 '17 at 16:15
  • Are you keeping a strong ref to these delegates somewhere? What's the class look like? – Ssswift Mar 28 '17 at 17:02
  • 1
    Don't instantiate a new URLSession every time. This is a known issue with URLSession. – Rob Mar 28 '17 at 17:05
  • Mozahler: that's not true. The URLSession isn't invoked at all unless a valid SSID is found; I.E. you are actually connected to a local network. Tried and tested, no problems there. Ssswift: no strong references. @Rob is there an alternative? Can I just tell the existing session to re-download the same file? – Tom Crews Mar 28 '17 at 17:10
  • 1
    To correct myself, the `URLSession` problem arises if you repeatedly instantiate, but never invalidate it. But you are invalidating it, so that's not likely to be the problem. Besides, the `URLSession` leaking problem is measured in kb, so it's unlikely to be the source of gb of memory consumption. It must be something else. I'd use "debug memory graph" feature in Xcode 8 and look for objects that you believe should be released, but weren't, and it will show you where the strong reference is (see http://stackoverflow.com/a/30993476/1271826). – Rob Mar 28 '17 at 20:29

1 Answers1

3

As Rob has said, put URLSessionConfiguration.default as a variable in your class and use that instead. I had the same problem and finally I solved it by using a singelton class which contains all my communication methods and set URLSessionConfiguration.default to a member variable. All my memory leaks were solved.

flame3
  • 2,812
  • 1
  • 24
  • 32
  • I need a little more information on that. If I move the task variable out of the function, I have to move the session variable out as well, because the task variable uses that session var. That's all fine, but then the session variable makes me change the `delegate: self` argument to `delegate: self as! URLSessionDelegate`. When I do that, the delegates don't get called anymore. Workarounds? – Tom Crews Mar 31 '17 at 22:11