When building apps for the iPhone, memory is always a concern. If you run out of memory, the OS will eventually kill your application off. While most developers work hard to do the right thing, and manage memory correctly, even if you follow the rules, you can still run out of memory.
UIImage imageNamed: is a good example of this. The method loads in an image from a file, and caches it, which gives you good performance. Unfortunately, there is no way to clear out that cached image. Even if you release the UIImage object, the cached bitmap will remain.
If you only have a small number of images in your program, and can handle having them all loaded in, this is not a problem. If, on the other hand, you have more images than can fit in memory, you have a problem. (Remember that the cache is storing the uncompressed images – which will be using WxHx4 bytes – if you have a 100×200 image, that’s 80K of memory.) This gets far worse when you have animation sequences – a simple 24 frame animation (which might only last one second) would eat up nearly 2 meg of ram. And that’s memory you can’t get back.
So what to do? There is a different instance method you can use with UIImage, namely initWithContentsOfFile: which will not do the caching. Unfortunately, this also has a side effect, in that when you want to use the Image, it needs to be decompressed at that point.
If you are using a single image for something, that’s probably acceptable. If you are using the images in an array to feed into an UIImageView via setAnimationImages, you are going to quickly notice a problem, depending on the size and number of your images.
The first time that you try to run the animation, the images will need to be uncompressed before they can be displayed. Worse, they will all need to be umcompressed before the first one is displayed.
This gets worse though. Lets say you have 60 images, each of which is going to display for 0.08 seconds. That’s 12.5 frames per second. When I did this, it was with images at a size of 200×200 pixels. I’d precooked the images so that they had numbers on them, so that I could see what showed up, and when. I figured that the decompression stage would result in my animation being delayed until it was complete. That was right. What I hadn’t figured would be that the internal timer that was deciding which frame to display didn’t wait.
- (IBAction) testAnimation {
UIImageView *myAnimation = [[UIImageView alloc] initWithImage:[[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"1" ofType:@"png"]]];
myAnimation.center = CGPointMake(160, 160);
[self.view addSubview:myAnimation];
[myAnimation setAnimationImages:animationArray];
[myAnimation setAnimationDuration:0.8];
[myAnimation setAnimationRepeatCount:-1];
[myAnimation startAnimating];
}
The net effect was that there was a delay, while nothing was displayed, then the first frame to be displayed was frame 7. If I ran the animation a second time, it displayed correctly, since the images had already been decompressed once.
Note that this was also only visible on the actual device (an 2nd gen iPod Touch). On the simulator, things ran fast enough that it wasn’t visible.
If we are trying to switch between sequences of animations, we can’t afford the delay, or missing frames if we don’t cache the images, and we can’t afford the memory hit if we use the caching.
The solution is to handle our own animation cycle, and force the decompression of the images as needed, ideally taking place while the previous image is still being displayed.
Double buffering would do this quite nicely, but what can we use as a back buffer? A simple thing would be to use an extra UIImageView, since that already knows how to do the decompression properly.
- (IBAction) startAnimationLoop {
self.showA = TRUE;
currentFrame = 0;
self.a = [[UIImageView alloc] initWithImage:[animationArray objectAtIndex:0]];
self.b = [[UIImageView alloc] initWithImage:[animationArray objectAtIndex:1]];
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.08f target:self selector:@selector(advanceAnimation) userInfo:nil repeats:YES];
}
- (void) advanceAnimation {
currentFrame = (currentFrame + 1)%[animationArray count];
if (showA) {
[i setImage:[a image]];
[b setImage:[animationArray objectAtIndex:currentFrame]];
}
else {
[i setImage:[b image]];
[a setImage:[animationArray objectAtIndex:currentFrame]];
}
showA = !showA;
}
In the code shown above, we’re really doing triple buffering – We have two off screen UIImageViews – a and b, which we preload with the first couple of images. we then alternate which one we pull the decompressed image out of, and copy it into the on screen UIImageView (i). Once we’ve done that, that frame of animation is visible. We now have 0.08 seconds before the timer will trigger, and we need to have the next image ready to go. We do that by asking the other UIImageView to set itself to the next image in the sequence.
Running with this, we wind up with no missed frames, and no noticeable delay before the first frame of animation starts to play.
I’ll clean up my complete project folder, and upload it to the Software Section later this afternoon.