NSURLCache Uses a Disk Cache as of iOS 5
While writing AFDownloadRequestOperation, a new subclass for AFNetworking, I discovered that the behavior of NSURLCache changed between iOS 4.x and iOS 5.x.
Before iOS 5, NSURLCache just saved requests to memory, even if the documentation said otherwise – the diskCapacity property was silently ignored. This led to some open-source subclasses of NSURLCache, which retrofit disk caching. Most popular is SDURLCache and my enhanced, faster fork of it. Even Apple has an example online that shows how to create a simple URLCache.
As of iOS 5, NSURLCache automatically saves responses to disk. I haven’t found anything in the release notes that confirms the change, but I tried it both with iOS 4.3/5.0/5.1 on the simulator and the device, and with every 5.x version, a disk cache file is created and populated. This is great, as many developers probably aren’t aware of this and the system just does the right thing on its own – and it’s fast:
If the Cache-Control headers indicate that this request can be cached, iOS automatically saves it to a local SQLite cache file in AppDirectory/Caches/(bundleid)/Cache.db. For example, public, max-age=31536000
marks that the request cache will be valid for a year, as max-age is listed in seconds.
The SQLite scheme for the cache looks identical to the one used in OS X:
PRAGMA foreign_keys=OFF; | |
BEGIN TRANSACTION; | |
CREATE TABLE cfurl_cache_schema_version(schema_version INTEGER); | |
CREATE TABLE cfurl_cache_response(entry_ID INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, version INTEGER, hash_value INTEGER, storage_policy INTEGER, request_key TEXT UNIQUE, time_stamp NOT NULL DEFAULT CURRENT_TIMESTAMP); | |
CREATE TABLE cfurl_cache_blob_data(entry_ID INTEGER PRIMARY KEY, response_object BLOB, request_object BLOB, proto_props BLOB, user_info BLOB); | |
CREATE TABLE cfurl_cache_receiver_data(entry_ID INTEGER PRIMARY KEY, receiver_data BLOB); | |
DELETE FROM sqlite_sequence; | |
CREATE INDEX request_key_index ON cfurl_cache_response(request_key); | |
CREATE INDEX time_stamp_index ON cfurl_cache_response(time_stamp); | |
CREATE INDEX proto_props_index ON cfurl_cache_blob_data(entry_ID); | |
CREATE INDEX receiver_data_index ON cfurl_cache_receiver_data(entry_ID); | |
COMMIT; |
So, why should you care? Well, the Cache.db caches any file that has a correct Cache-Control header set. Thus, if you download a PDF document, it might end up in your disk cache as well, taking up twice the memory.
The default NSURLCache will be used, with a disk limit of 20MB. You can easily test this with GDB/LLDB:
1
|
|
The memoryCapacity
defaults to 40MB, although the cache will clear itself in low-memory situations.
So for downloads that you manually save to disk, you might want to override the NSURLConnection delegate connection:willCacheResponse: and return nil:
// changes the default implementation in AFURLConnectionOperation to NOT cache per default, unless a block is set. | |
// we assume that downloads shouln't be saved into NSURLCache (which uses a disk-cache since iOS5) | |
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection | |
willCacheResponse:(NSCachedURLResponse *)cachedResponse | |
{ | |
if (self.cacheResponse) { | |
return self.cacheResponse(connection, cachedResponse); | |
} else { | |
return nil; | |
} | |
} |
When creating NSURLRequest using requestWithURL:cachePolicy:timeoutInterval:, you can define the cachePolicy, but this only allows you to choose if and how the cache will be read.
Available options are: only use the cache value (NSURLRequestReturnCacheDataDontLoad
); try the cache and load if different (NSURLRequestReturnCacheDataElseLoad
); or ignore the cache entirely (NSURLRequestReloadIgnoringLocalCacheData
).
The default option, if not set explicitly, is NSURLRequestUseProtocolCachePolicy
, which most of the time is equivalent to NSURLRequestReturnCacheDataElseLoad
. This uses the cache if the object hasn’t changed. There are a few other options in the enum, but those are unimplemented.
Note: There doesn’t seem a way to force caching of certain requests; connection:willCacheResponse: is only called if the response contains a Cache-Control header, according to Apple’s documentation:
The delegate receives connection:willCacheResponse: messages only for protocols that support caching.
Lastly, Apple suggests that caching can also be fine-tuned with subclassing NSURLProtocol, which indeed allows some interesting use cases, like providing a cache for UIWebView or decrypting files on the fly.
If you’re not yet using AFNetworking, you really should. It’s a big step forward compared to classical NSURLConnection handling, even if Apple recently added a few new fancy shorthands in iOS 5. In AFNetworking, your network operations are indeed subclasses of NSOperation, which allows much better control over what’s currently running, and AFHTTPClient is the perfect base class to implement any API.