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.
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:
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”.
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:
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.
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.
12 Responses
Good 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?
Unfortunately, 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
Steve, 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!
Kevin, 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.
Ah, I’ve been testing with the 4.3 simulator for backwards compatibility. Thanks for the heads up, I’ll try it on 5.x
Thanks 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.
Kevin, 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.
Well, 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
Julian, 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.
Where 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
Thanks 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
@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.