Memory leak when using NSURLSession.downloadTaskWithURL

i0S Swift Issue

Question or problem in the Swift programming language:

So I hit another roadblock in my endeavors with Swift. I am trying to load multiple images into an image gallery – all works fine except of one thing. The memory use of the application keeps growing and growing despite the fact that I clear the images. After eleminating basically all the code, I found out that this is caused by my image loading script:

func loadImageWithIndex(index: Int) {
    let imageURL = promotions[index].imageURL
    let url = NSURL(string: imageURL)!
    let urlSession = NSURLSession.sharedSession()
    let query = urlSession.downloadTaskWithURL(url, completionHandler: { location, response, error -> Void in

    })
    query.resume()
}

As you can see this piece of code does basically nothing right now. Yet every time I call it, my apps memory usage grows. If I comment out the query, the memory usage is not changing.

I have read several similar issues but they all involved the use of a delegate. Well, in this case there is no delegate yet there is the memory issue. Does anybody know how to eliminate it and what is causing it?

EDIT: Here is a complete test class. Seems like the memory grows only when the image could be loaded, like the pointers to the image would be kept in the memory for ever. When the image was not found, nothing happens, memory usage stays low. Maybe some hint how to clean those pointers?

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        //memory usage: approx. 23MB with 1 load according to the debug navigator
        //loadImage()

        //memory usage approx 130MB with the cycle below according to the debug navigator
        for i in 1...50 {
            loadImage()
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func loadImage() {
        let imageURL = "http://mama-beach.com/mama2/wp-content/uploads/2013/07/photodune-4088822-beauty-beach-and-limestone-rocks-l.jpg" //random image from the internet
        let url = NSURL(string: imageURL)!
        let urlSession = NSURLSession.sharedSession()
        let query = urlSession.downloadTaskWithURL(url, completionHandler: { location, response, error -> Void in
            //there is nothing in here
        })
        query.resume()
    }
}

I am sorry, I have no idea how to use the profiler just yet (being very noob in this whole iOS jazz), at least I will attach a screenshot of the profiler that is produced by the code above:

How to solve the problem:

Solution 1:

You have to call invalidateAndCancel or finishTasksAndInvalidate on the session, first of all… or else, boom, memory leak.

Apple’s NSURLSession Class Reference states in Managing the Session section in a sidebox:


IMPORTANT
— The session object keeps a strong reference to the delegate until your app exits or explicitly invalidates the session. If you do not
invalidate the session, your app leaks memory until it exits.

So yeah.

You might also consider these two methods:

  • flushWithCompletionHandler:


Flushes cookies and credentials to disk, clears transient caches, and
ensures that future requests occur on a new TCP connection.

  • resetWithCompletionHandler:


Empties all cookies, caches and credential stores, removes disk files,
flushes in-progress downloads to disk, and ensures that future
requests occur on a new socket.

… as directly quoted from the aforementioned NSURLSession Class Reference.

I should also note that your session config can have an effect:

- (void)setupSession { NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; config.URLCache = nil; config.timeoutIntervalForRequest = 20.0; config.URLCredentialStorage = nil; config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; self.defaultSession = [NSURLSession sessionWithConfiguration:config]; config = nil; } 

One key, if you are not using the [NSURLSession sharedSession] singleton, is for you to have your own custom singleton NSObject subclass that has a session as a property. That way, your session object gets reused. Every session will create an SSL cache associated to your app, which takes 10 minutes to clear, if you allocate new memory for a new session with each request, then you will see unbounded memory growth from SSL caches that persist for 10 minutes regardless of whether or not you invalidateAndCancel or flush/reset the session.

That’s because Security framework privately manages the SSL cache, but charges your app for the memory it locks up. This will happen whether or not you set your configuration’s URLCache property to nil.

For example, if you are in the habit of doing say, 100 different web requests per second, and each one uses a new NSURLSession object, then you’re creating something like 400k of SSL cache per second. (I have observed that each new session is responsible for appx. 4k of Security framework allocations.) After 10 minutes, that’s 234 megabytes!

So take a cue from Apple and use a singleton with an NSURLSession property.

Note that the reason the backgroundSessionConfiguration type saves you this SSL cache memory, and all other cache memory like NSURLCache, is because a backgroundSession delegates its handling to the system who now makes the session, not your app, so it can happen even if your app is not running. So it’s just hidden from you… but it’s there… so I would not put it past Apple to reject your app or terminate its background sessions if there is huge memory growth back there (even though Instruments won’t show it to you, I bet they can see it).

Apple’s docs say that backgroundSessionConfiguration has a nil default value for the URLCache, not just a zero capacity. So try an ephemeral session or default session and then set its URLCache property to nil, as in my example above.

It’s probably also a good idea to set your NSURLRequest object’s cachePolicy property to NSURLRequestReloadIgnoringLocalCacheData also, if you are not going to have a cache 😀

Solution 2:

I faced a similar issue in a recent app.

In my situation I was downloading a number of images from an API. For each image I was creating an NSURLSessionDownloadTask and adding it to a NSURLSession with an ephemeral configuration. When the task was complete I called a completion block to process the downloaded data.

Each download task that I added caused additional memory to be allocated (according to the Xcode debugger) that was not released at any point thereafter. When downloading approximately 100 images, the debugger had the memory usage as ~600 MB. The more download tasks, the more memory, that was allocated and not released. At no point had I displayed or used the image data in any way, it was simply stored to disk.

Trying to diagnose the issue in instruments was not fruitful. Instruments showed no leaks, and no allocations corresponding to the download tasks or image data. There were no memory leaks due to retain cycles in my blocks or the like, only in the debugger did the memory spiral.

After several hours of trial and error including:

  • Downloading the images with the completion block set to nil (to ensure I did not have a retain cycle in the block).

  • Commenting out various parts of my code to make sure that the allocations occurred precisely when ‘[downloadTask resume]’ was called.

  • Setting the URLCache for the session to both nil and a cache with a size of 0. As per: http://www.drdobbs.com/architecture-and-design/memory-leaks-in-ios-7/240168600

  • Changing the NSURLSession configuration to the default configuration.

Finally I switched my NSURLSession configuration from an ephemeral type to a background type. This required that I not use a completion block to process the images. I instead processed them in the delegate. This solved the issue for me. Adding download tasks to the background NSURLSession resulted in almost zero memory growth as the downloads were initiated and processed.

I wish I had a better understanding of why this is the case, but I unfortunately do not. I have seen this issue crop up for others as well and have yet to come across a better solution or explanation.

Solution 3:

In my project, it is because the default urlCache might consume some memory. If you want to reduce memory usage try to set the urlCache to 0

configuration.urlCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)

my whole urlSession looks like:

private var urlSession: URLSession = {
    let configuration = URLSessionConfiguration.default
    configuration.allowsCellularAccess = true
    configuration.httpShouldSetCookies = true
    configuration.httpShouldUsePipelining = true
    configuration.requestCachePolicy = .useProtocolCachePolicy
    configuration.timeoutIntervalForRequest = 60.0
    configuration.urlCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
    return  URLSession(configuration: configuration)
}()

Solution 4:

After URLSession resumes its data task ,If you no longer need a session, you can invalidate it by calling either
invalidateAndCancel() (to cancel outstanding tasks)
or finishTasksAndInvalidate()
(to allow outstanding tasks to finish before invalidating the object).
i think you are not invalidating the URLSession.So here memory leaks happen so you need to add any one of those above function and check in instruments in memory leaks sections .and your modified function will be like this

func loadImageWithIndex(index: Int) {
    let imageURL = promotions[index].imageURL
    let url = NSURL(string: imageURL)!
    let urlSession = NSURLSession.sharedSession()
    let query = urlSession.downloadTaskWithURL(url, completionHandler: { location, response, error -> Void in

    })
    query.resume()
    query.finishTasksAndInvalidate()
}

Hope this helps!