Nobody likes an update that worsens performance. And a game that can’t be played as a result of such an update is a Very Bad Thing. Probably all iPhone developers have had to deal with a crash or glitch that required them to scramble and hope that Apple would give them expedited reviews. I know I did recently for reMovem 2, when an update which introduced OpenFeint support crashed on users’ systems, but curiously not on mine. Funny how that slipped through the normal review process. But that’s another story.

So when iOS 4 came out recently I heard from a variety of people that it was generally “slow” on older devices. For my main iPhone 3G this was certainly the case, and I was convinced Apple couldn’t release such an update. No matter how you spin it, an update that makes the user experience worse cannot be a good thing. Well, it seems we have been left out in the cold on this one. App developers who rely on certain APIs have found out the hard way that even when the problem is in the OS, they are the ones left holding the bag.

I’m talking about CGContextDrawImage. My two most popular games, reMovem and reMovem free, made heavy use of this functionality to draw the balls on the screen. It’s not uncommon, especially for code ported from Mac OS X, where Quartz 2D is widely used for drawing. I’m not sure how this is possible, but CGContextDrawImage (and its cousin [UIImage drawInRect:]) is measurably slower on older iPhone devices. In my case, where it used to take less than 20 ms to render key elements of the screen on iOS 3.x, it now takes around 500 ms. That is, suffice to say, enough to cripple these games.

With all the commotion around the successful iPhone 4 launch recently I’m sure Apple would rather not worry so much about compatibility and performance on older CPUs. Fortunately you don’t have to wait for Apple to address the problem. I solved it in reMovem by using CALayer to draw the images I needed, and was able to get a performance boost at the same time. The same key elements of my game now render in around 2 ms. Here’s how.

The key to using CALayer is to understand that each UIView is backed by a Core Animation layer object. Each layer can have multiple sublayers. Each layer object has numerous properties, many of which are animatable. Simply attach a sublayer to your view’s existing layer, set its content property to your image, and you are done. Well, sort of. A good place to learn more about Core Animation is the excellent Cocoa Is My Girlfriend blog by Matt Long and Marcus Zarra.

Here’s a specific example. In my view’s drawRect: code, there’s a loop which needs to draw a bunch of images on the screen in various locations. For each image, it computes the rectangle to draw the image, then uses CGContextDrawImage (or [UIImage drawInRect:], your choice) to render the image to the screen. In Xcode:

// process rows from top -> bottom
for ( row = 0; row < numRows; row++ )
{
	// process columns left -> right
	for ( column = 0; column < numColumns; column++ )
	{
		id cell = [dataSource objectValueForRow:row column:column];
		int x = bounds.origin.x + column * width;
		int y = bounds.origin.y + row * height;
		CGRect cellRect;
		cellRect.origin = CGPointMake(x,y);
		cellRect.size = CGSizeMake( width, height );
		if ( cell != nil && cell != [NSNull null] )
		{
			CGFloat alpha = 1.0;
			if ( [cell selected] )
				alpha = 0.35;
			CGContextSetAlpha( context, alpha );
			CGContextDrawImage( context, cellRect, [[dataSource imageAtIndex:[cell imageIndex]] CGImage] );
		}
	}
}

Pretty simple stuff, almost identical to the original Mac code. Problem is, this loop, when profiled on iOS 4 took 500 ms on average. That’s way too slow to keep the game responsive, even if I only refresh the screen a couple of times per second. Well, obviously I can’t so that won’t ever happen. The game seemed to freeze and the images would hang in mid air for a bit before redrawing where they belonged.

Adding CALayer drawing to you code requires a little setup, but that can be done once in an init function. In mine, we create a bunch of sublayers, modify the properties appropriately, and add them to the view’s main layer. The tradeoff here is we require slightly more resources (sublayer memory) but we obtain vast improvements in performance. Why’s that? I imagine Apple’s engineers are spending tons of time optimizing Core Animation, which is literally behind everything you see on the iPhone and iPad screen. Betting on CALayer is a safe bet for the foreseeable future. Here’s the new code, which is not too different from the old code:

// process rows from top -> bottom
for ( row = 0; row < numRows; row++ )
{
	// process columns left -> right
	for ( column = 0; column < numColumns; column++ )
	{
		id cell = [dataSource objectValueForRow:row column:column];
		CALayer* layer = layers[row][column];
		if ( cell != nil && cell != [NSNull null] )
		{
			CGFloat alpha = 1.0;
			if ( [cell selected] ) 
				alpha = 0.35;
			UIImage* image = [dataSource imageAtIndex:[cell imageIndex]];
			layer.contents = (id)[image CGImage];
			layer.opacity = alpha;
		} else {
			layer.contents = nil;
		}
	}
}

 

As you can see the crucial change involves modifying the existing layer’s contents. Either we stuff the image into the layer or we set the contents to nil. This clears the layer, and nothing will be drawn. The resulting code fits nicely into the existing game logic and drawing code, so no other changes (besides the sublayer init code) need to be made.


The resulting drawing is also rendered better on the iPad in pixel double mode, and Core Animation does a much nicer job of resizing the images if needed. As an added bonus the drawing also has a smoother feel to it, with less jarring changes from one image/color to the next. I look at this as the silver lining to the cloud that was over my head for the last week or so. Future changes to the drawing code will be easier with CALayer drawing in place now. Once these changes were made and fully tested on all our devices, it was just a matter of getting Apple to review the updates in a timely manner. Luckily Apple responded to my request after three days, which is certainly one of the shortest turn-around times I’ve ever had.