4

I use a URLSession data task to download several JPG images from a backend. As the images are rather large in size (~500 KB) I want to cache the respective responses until they have expired (i.e. they have exceeded their max-age).

This is the code I use for downloading the images:

let request = URLRequest(url: url, 
                          cachePolicy: .useProtocolCachePolicy, 
                          timeoutInterval: 10.0)

let task = URLSession.shared.dataTask(with: request) { (data, _, error) in

    // Error:
    guard let imageData = data, error == nil, let image = UIImage(data: imageData) else {
        DispatchQueue.main.async {
            completion(nil)
        }
        return
    }

    // Success:
    DispatchQueue.main.async {
        completion(image)
    }
}

task.resume()

Curiously, this works great with caching for all images except for one. For some reason, this particular image is always downloaded again – its response is not cached.

The only difference between the responses that I can spot is that the image whose corresponding response is not cached has the biggest file size. While all other images are < 500 kB, this particular image is slightly > 500 kB.

I've played around with the shared cache size and set it to a ridiculously high value, with no effect:

URLCache.shared = URLCache(memoryCapacity: 1000 * 1024 * 1024, 
                           diskCapacity:   1000 * 1024 * 1024, 
                           diskPath:       nil)

I've checked that the Cache-Control header field is correctly set in the response:

Cache-Control: public, max-age=86400

and the Age header field is always below max-age, for example:

Age: 3526

What could be the reason for a single response not to be cached?

How can I fix this?

Arasuvel
  • 2,971
  • 1
  • 25
  • 40
Mischa
  • 15,816
  • 8
  • 59
  • 117
  • 1
    It's probably a good idea to implement https://developer.apple.com/documentation/foundation/nsurlsessiondatadelegate/1411612-urlsession manually and see if anything changes. – ilya n. Feb 26 '18 at 13:30
  • 3
    Regarding sizes, note the remark: _...responses are cached only when all of the following are true: ... no larger than about 5% of the disk cache size._ – ilya n. Feb 26 '18 at 13:32
  • That was my idea as well but then I cannot use the shared `URLSession` object (the delegate has to be assigned upon instantiation) and creating a custom `URLSession` might also change the behavior. – Mischa Feb 26 '18 at 13:32
  • I've read the note about sizes in the docs as well, but I think it's safe to say that an image of ~500 KB in size is less than 5% of the disk cache of 1000 MB... – Mischa Feb 26 '18 at 13:34
  • I don't think you can expect to get a disk cache if you pass in a nil path. You should probably fix that. But that still doesn't explain why it isn't caching it in RAM. – dgatwood Feb 27 '18 at 07:38
  • It's undocumented, but if the `path` parameter is `nil`, a default path is used. I could verify that by inspecting the App Data container and it's also explained in [another Stackoverflow post](https://stackoverflow.com/a/11332350/2062785). – Mischa Feb 27 '18 at 09:00

1 Answers1

1

This is not an answer to the question why the shared URLSession does not cache the image and I'm still grateful for any hints or answers to that question.

However, after experimenting some time with my code I figured out that (for whatever reason) the response is always being cached when I use a custom URL session with a default configuration rather than the default shared URL session:

let urlSession = URLSession(configuration: .default)

So if I use:

let task = urlSession.dataTask(with: request) { ... }

instead of

let task = URLSession.shared.dataTask(with: request) { ... }

the caching works as expected – whatever black magic is responsible for that.


I found a little hint in the docs for URLSession.shared though:

When working with a shared session, you should generally avoid customizing the cache, ...

In other words, if you’re doing anything with caches, cookies, authentication, or custom networking protocols, you should probably be using a default session instead of the shared session.

Mischa
  • 15,816
  • 8
  • 59
  • 117
  • NSURLSession caches are per session, and AFAIK, the only session that uses the shared cache is the shared session. So with those changes, unless you also pass in a custom session configuration object that provides a different cache, the code won't even trying to use the cache you created, but rather will just use an automatically created cache with some sensible default (unless you explicitly nil it out). But don't quote me on that. – dgatwood Feb 27 '18 at 07:45
  • Where would you take this information from? From all I know there is a single cache for each app which you can access through `URLCache.shared` and all `URLSession`s make use of it. It's automatically setup with a default cache but you can configure it by creating a custom `URLCache` object and assigning it to this property. This seems to be consistent with all the documentation I've read so far. – Mischa Feb 27 '18 at 09:18
  • I also encountered a similar scenario where i wanted to clear the cached content in requestCachePolicy but it dint seem to work. This answer helped me to understand some of the logics but not all. https://stackoverflow.com/a/45042040/2570153 – Umar Farooque Feb 27 '18 at 13:14
  • Normally, the way you would override the cache on a new session would be to assign a new cache object to sessionConfiguration.URLcache before creating the session. I am uncertain about whether other sessions use the shared cache; I thought they actually ended up with a private cache instance that just happens to point to the same on-disk data, but I could be remembering wrong. Either way, creating a new session with a different cache specified in the configuration definitely works, and will eliminate any doubt about whether your cache is actually being used by that session. :-) – dgatwood Feb 27 '18 at 17:32