Caching is a great way to improve application performance and end-user experience. Some popular iOS libraries try to reinvent caching, especially when it comes to storing images. They frequently overlook HTTP cache in Foundation’s URL Loading System.
Why HTTP cache? It’s an industry standard that fits the needs of most users. HTTP provides all kinds of tools for caching and chances are that your server already supports it.
In addition to on-disk cache it’s imperative to have a separate in-memory cache for fast access to decompressed images that are ready for display.
This guide focuses on images, but it most certainly applies to the other areas too. Its purpose is to answer some of the common questions about caching, and to provide references to a much more comprehensive sources.
- Each resource can define its caching policy via HTTP cache headers
- HTTP cache supports expiration and validation of cached responses
- Aggressive caching is a most viable strategy for static images, validation is useful for profile pictures, etc
- Foundation’s URL Loading System supports HTTP caching
- There are multiple ways to adjust URL Loading System’s cache management (NSURLRequestCachePolicy etc)
- NSCache can be used for in-memory cache, it requires proper configuration (total cost limit, cost per object)
There are several aspects of HTTP related to caching. In order to enable HTTP caching the server should attach proper cache headers to each response specifying the desired cache behavior.
HTTP cache is quite flexible. It allows servers to:
- Set restrictions on which responses are cacheable
- Set an expiration age for responses either using
max-age(part of composite Cache-Control header) and/or
- Provide validators (
Last-Modified) that are used to check stale responses with the server
- Force revalidation on each request
Here’s an example of what you should look for in HTTP response headers:
This response is cacheable and it’s going to be fresh for 1 hour. When the response becomes stale, the client validates it by making a conditional request using the
If-None-Match headers. If the response is still fresh the server returns status code
304 Not Modified to instruct the client to use cached data, or it would return
200 OK with a new data otherwise.
Most of the images are static assets that will not change in the future. The most viable caching strategy in this case is an aggressive caching. The server should simply set the
Cache-Control header with a
max-age value of a year in the future from the time of the request. It is recommended that
Expires should be set to a similar value.
Caching in Foundation’s URL Loading System
Foundation framework provides a set of classes for communicating with the servers using standard internet protocols including HTTP. It also implements cache management:
- It has a composite on-disk and in-memory cache
- It’s hip to cache control
- It handles revalidation transparently, you never have to deal with status code 304 (not modified)
There is no addition configuration on the client side required to make cache management work. However, there are certain things that you can do to make sure that the system would work the way you expect.
According to the Apple’s documentation, the responses are cached only when all of the following are true:
- The request is for an HTTP or HTTPS URL (or your own custom networking protocol that supports caching).
- The request was successful (with a status code in the 200–299 range).
- The provided response came from the server, rather than out of the cache.
- The session configuration’s cache policy allows caching.
- The provided
NSURLRequestobject’s cache policy (if applicable) allows caching.
- The cache-related headers in the server’s response (if present) allow caching.
- The response size is small enough to reasonably fit within the cache. (For example, if you provide a disk cache, the response must be no larger than about 5% of the disk cache size.)
Your app should already comply to most of them by default (for instance, the session configuration allows caching by default). However, one thing that a client should definitely do is to set an appropriate cache size.
Let’s dive a little deeper into URL Loading System and see which parts of it we can use to modify its caching behavior.
One of the classes in this system is a NSURLCache which is a part of cache management. It provides a composite in-memory and on-disk cache. It provides methods to configure cache size and its location on disk. It also has methods to manage
NSCachedURLResponse objects that contain the cached responses. This class isn’t that useful by itself but it’s important how it fits into the entire system.
The primary interface for URL Loading System is NSURLSession. It is a huge improvement over
NSURLConnection which was deprecated in iOS 9. Some of those improvements apply to caching too.
NSURLSession gives you a way to provide per-session cache (
NSURLCache), cache policy (
NSURLRequestCachePolicy), and set other options.
Let’s focus on a NSURLRequestCachePolicy which is a primary way for modifying caching behavior. The cache policy can be set either per
NSURLRequest, or per
NSURLSession. The default value is
.UseProtocolCachePolicy which works as described in the example in “HTTP Caching” section. Some of the other most useful policies are:
.ReturnCacheDataDontLoad- existing cache data should be used, regardless of its age or expiration date. If there is no existing data in the cache, no attempt is made to load the data. This policy might be useful when your app is in “offline” mode and you want to show stale images without having to validate them with a server.
.ReturnCacheDataElseLoad- similar to the previous policy, but allows client to load data from the server. Sometimes it’s convenient to set this property as a default to enable aggressive caching without worrying about “offline” mode. Note that this policy effectively disables cache validation.
.ReloadIgnoringCacheData- always load data from the server. This option prevents cache data from ever being used, you can’t use it to force validation.
NSURLSession also provides a comprehensive set of delegate methods. One those methods is URLSessionSession(_:dataTask:willCacheResponse:completionHandler:) from
NSURLSessionDataDelegate which. It might be used to prevent caching of specific URLs, providing a custom
userInfo for cache responses and more. Note that this method is called only if the
NSURLSession decides to cache the response. You can’t use it to force
NSURLSession to cache responses which headers explicitly disable caching.
One of the other great things about
NSURLSession is that it has its own way of limiting a number of concurrent connections via
HTTPMaximumConnectionsPerHost property of the
NSURLSessionConfiguration. It only limits the number of HTTP connections and not the number of concurrent session tasks (
NSURLSessionTask). Given that, if the client start a new request which can be served by a fresh cached response then it would be served immediately, no matter how many other tasks are executing at the given moment.
Again, I’ve just scratched the surface here, definitely check out URL Session Programming Guide if you haven’t done that yet.
The downside of
NSURLCache is that its in-memory cache is not very useful because it stores instances of
NSCachedURLResponse class which contain unprocessed
NSData. We don’t want to pollute precious RAM with that. What we need is a memory cache for fast access to decompressed images ready for display. Foundation already provides a class to do just that -
NSCache. It is very straightforward to use, however it has some caveats.
Starting with iOS 7
NSCache will no longer remove cached objects automatically unless you explicitly set either its
countLimit. The total cost limit is more flexible than the count limit, however, it requires clients to provide a reasonable cost with which to associate each cached object.
The obvious total cost limit is the number of bytes in memory. It might be computed as a percentage of available memory:
Now let’s compute a cost for
UIImage object. Most of the space taken by
UIImage is a bitmap which can be used to approximate its size in memory:
This configuration will provide arguably the best
NSCache performance. Memory cache will hold a lot of images while still being under the certain limit. It’s also important to immediately dispose of all cached objects when the app receives a memory warning.
The downside of a separate memory cache is that it doesn’t have any expiration and validation mechanisms. It’s fine for most use cases, because
NSCache evicts objects rather frequently. If you need more control over memory cache you can easily implement such features (expiration age, request policy, etc).