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:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
    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:

- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
    [[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

  1. Kevin
    February 3, 2012

    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?

    Reply
    • Todd Huss
      April 1, 2012

      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.

      Reply
      • Kevin
        April 12, 2012

        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.

        Reply
        • Todd Huss
          April 15, 2012

          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.

          Reply
          • Todd Mathison
            September 28, 2012

            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

  2. Steve Johnson
    March 26, 2012

    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

    Reply
    • Todd Huss
      April 1, 2012

      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!

      Reply
      • Steve Johnson
        April 1, 2012

        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

        Reply
  3. Julian
    June 29, 2012

    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

    Reply
    • Christopher Pickslay
      June 29, 2012

      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.

      Reply
  4. Edu
    March 7, 2013

    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:

    // Create an NSString from the local HTML file
    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:

    var nImg = 0;

    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:

    // Initialize the Cache, so we can control the amount of memory it utilizes
    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

    Reply
    • 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.

      Reply
      • Edu
        May 27, 2013

        Sorry, I have not managed to get any solution yet. I am still looking for an answer. Thanks for your interest.

        Reply
        • Christopher Pickslay
          May 27, 2013

          I 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.

          Reply
  5. 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.

    Reply
    • Edu
      July 31, 2013

      Unfortunatle, 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.

      Reply
      • Edu
        July 31, 2013

        *Unfortunately… Sorry for the mistype :$

        Reply
        • Toufic
          August 21, 2013

          I 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

          Reply
  6. Freddy
    August 30, 2013

    Thank 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!

    Reply
  7. kuldeep
    October 25, 2013

    Hi 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

    Reply
  8. Ramesh Chandran A
    July 15, 2014

    Hi 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?

    Reply
  9. Sjoerd Perfors
    December 22, 2014

    Hmm 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

    Reply
  10. Bruce
    April 30, 2015

    Thats a great tip thank you.

    Reply
  11. Glenn
    August 7, 2015

    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.

    Reply

Leave a Reply to Bruce

Cancel Reply