Dan Wood: The Eponymous Weblog (Archives)

Dan Wood Dan Wood is co-owner of Karelia Software, creating programs for the Macintosh computer. He is the father of two kids, lives in the Bay Area of California USA, and prefers bicycles to cars. This site is his older weblog, which mostly covers geeky topics like Macs and Mac Programming. Go visit the current blog here.

Useful Tidbits and Egotistical Musings from Dan Wood

Categories: Business · Mac OS X · Cocoa Programming · General · All Categories

Wed, 28 Feb 2007

File Caching, take 2: using NSURLCache directly

Recently I blogged about a home-spun cache technique. After some discussion in the comments, Robert Blum sent me a snippet of code which uses NSURLCache and related classes. I had no idea these classes could really be used for anything outside of the URL loading system, so I explored them a bit further, and with the help of some other code I found on CocoaBuilder, I got something working.

I've defined two category methods on NSURLCache. My code calls cachedDataForPath: to look and see if the data have already been cached; if not, it is generated and then saved for next time using cacheData:forPath:. Note that this code generates a file:// URL; I'll explain why after the code.


- (void) cacheData:(NSData *)aData forPath:(NSString *)aPath;
{
    NSURL *cacheURL = [NSURL fileURLWithPath:aPath];
    NSURLResponse *response = [[[NSURLResponse alloc]
                  initWithURL:cacheURL
                     MIMEType:@"application/octet-stream"
        expectedContentLength:[aData length]
             textEncodingName:nil] autorelease];
    NSCachedURLResponse *cachedResponse = [[[NSCachedURLResponse alloc]
        initWithResponse:response data:aData] autorelease];
    NSURLRequest *request = [NSURLRequest requestWithURL:cacheURL];
    [self storeCachedResponse:cachedResponse forRequest:request];
}

- (NSData *)cachedDataForPath:(NSString *)aPath;
{
    NSURL *cacheURL = [NSURL fileURLWithPath:aPath];
    NSURLRequest *request = [NSURLRequest requestWithURL:cacheURL];
    NSCachedURLResponse *resp = [self cachedResponseForRequest:request];
    return [resp data];
}

I discovered a couple of interesting issues in this technique. My first cut at the code used an ad-hoc, unregistered URL scheme created with -[NSURL initWithScheme:host:path:]. The data seemed to get cached fine, but retrieving it was another matter. If I made a new NSURLRequest out of the identical URL, the NSCachedURLResponse could not be found using -[NSURLResponse cachedResponseForRequest:]. Perhaps this was the case because there was no NSURLProtocol to treat this scheme as a recognized URL type. When I switched to a file URL scheme, even if the given path does not correspond to a real file, the caching and lookup worked fine.

The other issue I ran across was in disk caching. The main reason I am caching these data is so that they are stored on disk for faster access next time I run the application. Unfortunately, NSURLCache doesn't give you much control over writing to disk. Using the shared URL cache retrieved from +[NSURLCache sharedURLCache], the cached data appeared on disk after about 15 seconds of idle time. Fair enough. But when I created my own URL cache (which I want to do so that my cache can be used across applications), everything worked except the cache would never be saved to disk! The cache folder would remain empty no matter what. I don't know if that is a bug or a feature.

The Next Step...

This was a useful exercise, and it's nice that I'm now using Cocoa's caching mechanism. But it feels like I'm going behind Cocoa's back by managing the cache myself, faking URL responses just to get my data cached. So the next step I plan on taking, eventually, is to go ahead and make up my own URL scheme and let the caching system manage everything. When I want the data, I'll construct the appropriate URL and ask for the data. I'll put the data generation into that URL protocol.

Once I get it reworked that way, I'll be happy to report my experiences here.

Update: Inspired by something Brent Simmons posted, I figured out why I couldn't get my new cache to write to disk. In my case, I was neglecting to call +[NSURLCache setSharedURLCache:], which I had assumed I wouldn't need since I was calling the cache I created directly. Unfortunately, this defeats the whole purpose of having a specific cache, if it's going to be the shared cache for the entire application. I really wanted to be able to specify that certain kinds of data go into a particular cache, but not all my application's data! This problem has been filed with Apple as bug ID 5031377