Okay, I can reproduce it.
The reason it dies is because the operating system believes the application is using too much memory (> 24MB of graphical memory on iPhone 2g/3g). Before the OS realizes you have freed the memory consumed by the images in the cache, another image gets loaded in. This could be due to some internal race condition or maybe it is just erring on the side of memory safety. I don’t really know, but I can offer some suggestions:
In reality, you would have to have a pretty odd reason to have more than one 1024x1024 (or however large) image in memory at a time. In a more-real, less-teachy application, you would immediately cut an image down to a size that fits on the screen better when it is loaded from disk or grabbed from the camera. This, of course, only makes the problem less likely because you are using less memory per image - effectively delaying it, perhaps indefinitely if the user doesn’t create hundreds of images.
You could use a smarter image cache that didn’t rely on memory warnings. I’m thinking of a FIFO queue that you monitor by keeping track of how much memory you are consuming and freeing an image or two at a time before a low memory warning occurs. For example, the image cache would record the size of an image (4 * width * height is the number of bytes of an image, plus a tiny bit of overhead for the UIImage object internals) when added to the cache and add that number to its running total. When you get close to some threshold (24MB is where Apple cuts you off on the original iPhone and 3G, but you must account for other image objects as well), you could drop the first one or two images loaded in. Now, because you don’t know what other objects might still have a hold on images in the cache (like the image view), you might want to check the retain count of images you plan to remove from the cache and only remove those that have a retain count of 1.
You could also, on viewWillDisappear: in ItemDetailViewController.m, try setting the imageView’s image property to nil. When the image cache remove all of its images, the image that was last displayed in the imageView will still be resident in memory because the imageView maintains a hold on it. This extra few megabytes might be the tipping point for the OS shutdown (in fact, the more I think about it, the more I believe that is true).
Now, for some fun Objective-C twiddling I did to confirm that this was the problem. I wanted to make sure that the UIImages were immediately being deallocated and not added to an autorelease pool, and I wanted to log the order of things. So I needed to know exactly when the UIImage was dealloc’ed (and if it was autoreleased, waiting for release + dealloc).
Every Objective-C object has an instance variable called “isa”. This is a pointer to the class that is the object’s type. When an object gets send a message, it passes that info to the class that is pointed to by this isa pointer, and the method that matches the message selector in that class is executed (with the implicit variables self set to the instance sent the message and _cmd set to the selector).
So, if I were to create a UIImage object as normal, and then change its isa pointer to some UIImage subclass that I created, I could override retain, autorelease, release and dealloc to NSLog what was happening. When the image object, which would now be of my new subclass type, was sent one of these messages, I could see some debug logs. The implementation of that class, whose interface is declared as ImageMemory : UIImage, looks like this:
NSLog(@"%@: Retain (%d)", self, [self retainCount] + 1);
return [super retain];
NSLog(@"%@: Autorelease (%d)", self, [self retainCount]);
return [super autorelease];
- (oneway void)release
NSLog(@"%@: Release (%d)", self, [self retainCount] - 1);
NSLog(@"%@: Dealloc", self);
Of course, the isa instance variable is protected so I couldn’t just create an image and change its isa pointer wherever I wanted to. That is to say, I couldn’t do this:
UIImage *img = ...;
img->isa = [ImageMemory class];
But, any instance can access its own protected instance variables, so I added a category to UIImage to change its isa pointer.
@implementation UIImage (Swizzle)
self->isa = [ImageMemory class];
Then, I changed the code in ImageCache.m for the method imageForKey: to the following:
result = [[UIImage alloc] initWithContentsOfFile:pathInDocumentDirectory(s)];
Anyway, I thought that might be a fun thing to know. You also might be wondering why I didn’t just create a category for UIImage that replaced autorelease, retain, release and dealloc: if I did this, I couldn’t call UIImage’s implementation of these methods. They are gone for good and I have replaced them. If I were to naively call [super release] in my replacement release method, it would go to NSObject and not UIImage.