Update: I was contacted by an Apple engineer saying this bug was fixed in 3.0, and asked if I could reproduce it there. I tried my sample project again, and I was unable to reproduce it on 3.0. This problem only seems to affect 2.x devices.

Not too long ago I was profiling an iPhone app that I had written for a client. Their testers had found that using a certain feature for long enough ended up causing the app to be ejected because of a low memory situation.

After some quality, yet painful, time with Instruments I discovered I was never deallocating image memory in the form of CGImageRef and its internal drawing cache. (It turns out that if you draw a CGImageRef while on a thread other than the main thread, it creates a cache of the bitmap data. However, this wasn’t my problem.)

At first I thought I simply forgot to release the UIImages somewhere. I put in some logging code and was able to confirm that I was releasing the UIImages the correct number of times, in the right places. But it appeared that someone had called retain on it an extra time.

That’s when I happened to notice that only the UIImages I had constructed with imageNamed: were failing to be released. I suspected that they were being retained in a global cache. The only problem with this cache is not released in low memory situations. Even when my app receives a low memory notification and I free up all the memory I can, the UIImage global cache just sits there clutching it’s unused UIImages to its chest, muttering.

Unfortunately for me, some of the UIImages I was loading were large, and I was expecting them to be deallocated when I released them. As the feature was used, different UIImages were loaded with imageNamed: and never released until the phone ran out of memory and my app was ejected.

The solution was to write a replacement method for imageNamed: that didn’t cache the UIImage. Here’s what it looks like:

@implementation UIImage (OrderNDev)

+ (id) imageNamedNoCache:(NSString *)name
{
	NSString *basename = [name stringByDeletingPathExtension];
	NSString *extension = [name pathExtension];
	NSString *path = [[NSBundle mainBundle] pathForResource:basename ofType:extension];
	return [[[UIImage alloc] initWithContentsOfFile:path] autorelease];
}

@end

The moral of the story is imageNamed: is only for use with small images that are used constantly throughout the app’s lifetime. Don’t use imageNamed: if you want that memory back on a 2.x device. Everything will work fine on a 3.x device though.