Reduce iOS memory utilization by taming NSURLCache
If your iPhone or iPad app embeds UIWebViews or makes HTTP requests directly with NSURLConnection, it’s important to keep an eye on memory utilization by running it through the profiler occasionally. Web requests can use a lot of memory, and you may find the memory footprint grows and grows the more you use the app. One of the leading causes we see of high memory utilization in an app is failing to explicitly configure the NSURLCache.
Configuring the shared cache
First off, let’s configure the cache when the app starts (before any requests are made) so we can control the amount of memory it utilizes. In your UIApplicationDelegate:
int cacheSizeMemory = 4*1024*1024; // 4MB
int cacheSizeDisk = 32*1024*1024; // 32MB
NSURLCache *sharedCache = [[[NSURLCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:@"nsurlcache"] autorelease];
[NSURLCache setSharedURLCache:sharedCache];
...
The above configures a 4MB in-memory cache with a 32MB disk cache. The disk cache will reside in the default iOS cache directory in a sub-directory called “nsurlcache”.
Responding to memory warnings
The most frequent cause of crashes we’ve seen in apps that use web views is being ejected for not freeing up enough memory when a memory warning comes in. When your app receives a memory warning, you should purge the shared cache to free up memory. Do this in your UIApplicationDelegate:
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}
Now when you run your app in the profiler, you should see the memory utilization flatten out, and if you trigger a memory warning in the simulator you should see the memory usage drop.
A word of warning
There are a lot of recommendations on StackOverflow about purging the NSURLCache by recreating it, however, we’ve seen this lead to occasional crashes when requests occur on another thread while the cache is being recreated. Our advice is therefore to create the cache once when the app starts and purge it when appropriate.
25 Comments
Kevin
February 3, 2012Good article.
What’s default behavior of system NSURLCache policy? And what result will be expected if we use system behavior and do nothing?
How to calculate all memory size taken up by NSURLCache during app running?
Todd Huss
April 1, 2012Kevin, if you use the system behavior the cache data objects will grow very large and will not shrink on memory warnings. I’m not sure what the upper limit is but if you run your app through the memory profiler and keep making requests you’ll see it memory utilization grows and grows and grows taking up more and more memory. When you set the limits explicitly you’ll see your memory utilization flatten out when the cache is filled.
Relying on the default system behavior will result in your app being ejected by the OS more often than it should in low memory situations which is why we recommend always setting the cache limits explicitly and clearing the cache on memory warnings.
Kevin
April 12, 2012Thanks you guy.
Plus one more question, my app is based on HTML 5, support offline feature by manifest, so you will always see file ApplicationCache.db in your app directory, does it also obey with the rule of NSURLCache? Is it the same on general principle? Thanks.
Todd Huss
April 15, 2012Kevin, HTML 5 offline storage is completely independent of the shared NSURLCache so your cache settings will have no effect on your HTML 5 data store.
Todd Mathison
September 28, 2012Where are HTML 5 resources cached and how do you clear it? I deleted both the Cache.db and the ApplicationCache.db files and my page was still cached.
Thanks!
Todd
Steve Johnson
March 26, 2012Unfortunately, this appears to not be true. According to these sources, disk caching on iOS is not possible – and the documentation is misleading by even suggesting it.
http://openradar.appspot.com/8991238
http://books.google.com/books?id=K28J48Gia3UC&pg=PA231&lpg=PA231&dq=nsurlcache+example&source=bl&ots=QupNo3ZT8m&sig=WkKfpnU0E7esxz2TfRjnjWE6B8I&hl=en&sa=X&ei=jOpwT-G8GMe42QXC9qTxAQ&ved=0CCUQ6AEwATgU#v=onepage&q=nsurlcache%20example&f=false
Todd Huss
April 1, 2012Steve, starting with iOS 5.x when I declare a disk cache it does indeed create a cache file in Library/Caches/my.app.id/nsurlcache. Whereas in 4.x it does not create a cache file.
The good news is that either way there’s no downside to declaring a cache file!
Steve Johnson
April 1, 2012Ah, I’ve been testing with the 4.3 simulator for backwards compatibility. Thanks for the heads up, I’ll try it on 5.x
Julian
June 29, 2012Well, i tryed to use NSURLCache those days, because i would like to cache a JSON response which is 478.52KB GZipped and 2.88MB openend.
I tryed to set-up my own sharedcache by 4MB in memory (also tryed 10MB) and 20MB on disk, but the cache never seems to trigger. Only requests which are 500KB or lower.
I tryed this on my iPhone 3GS (where 128MB is the maximum empty RAM i can obtain, over 256MB) with iOS 5.1.1.
I’ll try to make a test with memory profile to see what’s happening exactly, but if it’s the case, this is pretty useless. In fact I’m actually MKNetworKit which makes a dump of memory when it triggers (and cache work pretty well).
Do you have any idea on what’s happening? On Stackoverflow no one seems having this problem (apart this one http://stackoverflow.com/questions/7166422/nsurlconnection-on-ios-doesnt-try-to-cache-objects-larger-than-50kb).
Thanks,
Julian
Christopher Pickslay
June 29, 2012Julian, it’s certainly possible that there’s some such limitation in the default NSURLCache implementation. You could try rolling your own cache subclass or using one of the existing ones mentioned in the replies to that SO question.
You might also check the cache-control headers on your HTTP response. It’s possible your server is telling the client not to cache.
Edu
March 7, 2013Thanks for this amazing post. It is very clear. Although, I have been trying to understand how this works with a UIWebView and I cannot really understand how the UIWebView is using this NSURLCache sharedURLCache.
My scenario is this one:
I have implemented a test app for iOS 5 where I have a single view with a UIWebView inside. This UiWebview is going to load a local index.html file like this:
NSString *fullURL = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"www"];
NSURL *url = [NSURL fileURLWithPath:fullURL];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];
// Load local HTML file
[self.webView loadRequest:requestObj];
In this index file, I have a JS script that downloads a JSON from Flickr with the last public images uploaded to the public feed. Each image is appended as an inside a list. This process is repeated every 10 seconds, until we reach 1000 images downloaded from the feed. See the code:
function getData()
{
console.log('Adding more pics!!');
$.getJSON('http://api.flickr.com/services/feeds/photos_public.gne?format=json&jsoncallback=?', function(data)
{
var items = [];
$.each(data.items, function(i, image) {
items.push('');
});
$('#page' + activeView + '> #imagesList').append(items.join(''));
nImg += items.length;
});
$('#page' + activeView + ' > #numImages').html(nImg);
if(nImg < 1000)
setTimeout("getData();", 10000); // Get more data in 10s
}
$(document).ready(function()
{
getData();
});
Finally, in my AppDelegate, I set up the sharedURLCache as written in this post:
int cacheSizeMemory = 4*1024*1024; // 4MB
int cacheSizeDisk = 32*1024*1024; // 32MB
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
But, despite what it is supposed to happen, when I open instruments and run the app checking the allocations, the memory keeps growing while we download images (up to 20MB), instead of flattening around 4-6MB, that is what I would expect if the UIWebView would be using this sharedURLCache to cache the images.
Does anybody know what am I doing wrong? Do I have misunderstood something? Am I loading the page wrong? Does the UIWebView uses another cache for the images loaded in the JS?
Please, let me know your thoughts. I can share the whole project if necessary, because I really want to understand how this sharedURLCache works, and how the UIWebView uses it, in case it is used at all.
Thank you very much.
Edu
aki
April 14, 2013@Edu , is the issue what you have posted in this forum got solved ? If so what was the solution ? Would you please reply because I am too facing the same kind of issue. Thanks.
Edu
May 27, 2013Sorry, I have not managed to get any solution yet. I am still looking for an answer. Thanks for your interest.
Christopher Pickslay
May 27, 2013I don’t think this is a reasonable way to test whether the cache is working. If you’re loading images into a web view, each of those images requires memory to render. So you should expect to see the app’s memory grow as it loads more images. The same thing would happen if you loaded the images into a web view from disk.
If you want to prefetch images to cache without loading them into memory, you might try loading them in the background with an NSURLConnection.
Franck
June 13, 2013@Edu: Did you finally found a solution. Because I have a VERY similar problem, and just cannot get a solution that work.
Edu
July 31, 2013Unfortunatle, I have not managed to get any working solution yet. I understand what @ChristopherPickslay says and it seems reasonable, but I have not tried it yet. Thanks for your interest and let me know if you find anything, please.
Edu
July 31, 2013*Unfortunately… Sorry for the mistype :$
Toufic
August 21, 2013I am facing the same issue , can i know please if you got any solution on how to free the memory ? my application memory is growing up and it is causing the application to be crashed when using iPad1. Thanks in Advance
Freddy
August 30, 2013Thank you for this informative post. It finally helped me find a solution to the problem of downloading a whole bunch of images in an NSOperationQueue/NSOperation where the NSData object used to download the image never seems to get released. I did exactly as you said, putting the sharedCache in the app’s launch and then I do removeAllCachedResponses after each image is downloaded (I do this in the main thread using a callback) and my memory problems have completely disappeared. Amazing! Thanks again!
kuldeep
October 25, 2013Hi All,
I am also different kind of problem with respect to cache :-
I am successfully able to cache the data using the NSURLCache policy but below case i am not getting the cache data
1.Download some data in online mode.
2. remove app from sleep mode.
3.on the airplane mode(offline).
4.Wait till the device lock
5.Unlock the device and launch the app
6.Showing network error since no data
Ramesh Chandran A
July 15, 2014Hi Todd, thanks for the documentation.
This documentation is very helpful. I hope this will help us to reduce the Virtual Memory foot prints of a UIWebView and hence by reducing the overall memory usage of the App?
I also have a very basic question that this cache memory is applicable for a single webview? If I have multiple webviews in an App the cache memory also will get multiplied?
Christopher Pickslay
July 17, 2014It’s a single cache shared by all UIWebViews in your app.
Sjoerd Perfors
December 22, 2014Hmm still have issues, to bad the instruments screenshots don’t show the NSURLCache so I dont really know where to look in instruments.
My question regarding where to look in instruments and how to fix it is on stackoverflow: http://stackoverflow.com/questions/27610373/nsurlconnection-is-growing-in-instruments-is-this-nsurlcache
Bruce
April 30, 2015Thats a great tip thank you.
Glenn
August 7, 2015Now when you run your app in the profiler, you should see the memory utilization flatten out, and if you trigger a memory warning in the simulator you should see the memory usage drop.