If you’re developing a game for Android that uses canvas drawing, you’re probably calling
Canvas.drawBitmap to draw many bitmaps to the screen. You also may have noticed that as your number of bitmaps per frame increases, your framerate drops significantly, to the point of your game becoming unplayable. You’ll also find that your game runs fast on some devices, and slowly on others (in particular, phones versus tablets). In this article on speeding up
Canvas.drawBitmap, I’d like to point out some pitfalls you can avoid, based on my own experience developing an app with heavy canvas drawing.
The game loop
Most likely, your game will have a game loop that runs as a separate thread. There are lots of examples on the web of standard game loops, and they tend to look like this:
The dedicated thread for your game loop executes the
run() method. This method repeatedly does the following:
- Lock the canvas
- Update the scene
- Draw the scene
- Unlock and post the canvas
Some people separate the scene update code from the scene drawing code (in another thread). After all, to give the game a consistent speed it’s necessary to update the scene at a stable frame rate (say, exactly 60 times per second) because moving objects that speed up or slow down hurt the player’s game experience. Assuming that the update code is very fast (a few milliseconds per frame) compared to the drawing code, this may indeed be useful. You can allow the drawing code to skip a frame once in a while, but you cannot allow the scene update code to do so.
In the process of increasing the frame rate of my game, I implemented various timers to carefully check the speed of locking the canvas, updating the scene, drawing the scene, and unlocking/posting the canvas. The results were surprising and may help you to understand why your game is slow under certain conditions:
- I’ve noticed that locking the canvas was very fast on different emulators (about 1 ms, from small screens to large screens), but on a real Samsung Galaxy Tab (1280×800) it’s about 9 ms. On the other hand, on a Lenovo P1c72 (1920×1080) is was 1ms – it seems not to be related to the screen resolution.
- Also, unlocking and posting the canvas was fast on an emulator for small screens (1 ms), very slow on an emulator for large screens (40 ms), and fast on all real devices that I have tested (Samsung Galaxy Tab, Lenovo P1c72, Samsung DUOS, Alcatel).
- On real devices, the speed of the drawing code seems to be directly relative to the screen resolution. On small screen devices I got high speeds (60 fps), while on larger screens the frame rate dropped dismayingly fast. However, on an emulator with a large screen drawing was very fast, but this may be because my PC has a fast graphics card.
synchronized is used around the
SurfaceHolder to make sure that no other code accesses this variable while drawing is going on. The Android documentation, meanwhile, indicates that only one thread may every draw to a surface at any time, so it’s unlikely that there is any interfering code. If there were,
synchronized would have the VM put the other code on hold until it can access the surface. For what it’s worth, I have tested my game loop code with and without
synchronized and seen no speed difference.
Double-buffering is slow
Reading Mario Zechner and Robert Green’s book Beginning Android Games, I saw that they suggest that you create a full-screen bitmap (a frame buffer), then for each frame draw all your game bitmaps to this buffer and draw the buffer to the canvas. The advantage of this, they say, is that any scaling of the final image to accommodate different device screen aspect ratios can be done by scaling the frame buffer and not the individual images.
While it is true that this does simplify the calculations you need to make, I have noticed that the extra call do
drawBitmap to draw this (large) bitmap to the canvas causes a significant frame rate reduction (I seem to recall losing about 30% of my frame rate), so it’s my opinion that this isn’t recommended. I would prefer to do all required scaling of bitmaps once, at the initialization stage, and keep the higher frame rate. (Please note that I can otherwise wholeheartedly recommend you read this book.)
Whether you use a frame buffer or write directly to the
SurfaceHolder’s canvas, you’ll need to clear the screen every frame by filling it with a solid color – or do you? In my case, the bitmaps I draw for each frame always cover the entire background, so there’s no need to clear it. While this is a very tiny speed improvement, every little bit helps. Consider whether you need to call
Canvas.drawColor or not.
Matrices and prescaling
While on the subject of scaling, Android offers a slew of matrix calculations that you can use to translate, rotate and scale your images as you draw them using
drawBitmap. Once you get in the habit of using matrices, it yields very clean code so I’m quite happy with them. However, while translating is fine (it’s probably no different from calculating the x’s and y’s yourself), it will come as no surprise that scaling and rotation are expensive. You should try to do all scaling and rotation at initialization and not each time you’re drawing a frame.
Android runs on many devices, and there are many device resolutions. What’s more, not only are the resolutions different, but the aspect ratios also differ. Some statistics can be found here. In order to accommodate different resolutions and aspect ratios, your bitmap assets must be scaled up (or down), and this is best done in the game initialization routine and not for every frame, as mentioned above. I’ve used the following procedure to prescale my images.
We assume that the original background image always has a square aspect ratio (1:1). This way, whether the Android device is in portrait mode or landscape mode, we always need to crop, but on average losses are less.
The original images are always scaled down (or up) in accordance with the resolution of the device. Scaling occurs first, then cropping. Cropping only applies to background images and planes, not animations. Using square images for backgrounds and planes not only results in less cropping, but also simplifies the scaling algorithm.
Width and height are always equal because the source aspect ratio is 1:1. For non-square images (like all animations), the scaling algorithm is:
Scaling is done for all images before the game starts up. If possible, resulting images are stored locally for portrait and landscape modes for reuse, so that the process only occurs once (although it’s not very resource intensive). At the very least, the scaled images remain in memory during the time that the app is visible and active in Android.
Bitmap Color Depth
A non-obvious pitfall is the color depth of your bitmap images. A typical PNG image has a color depth (or “pixel format” in Android Lingo) of 32 bits, where red, green and blue values occupy 8 bits each and 8 bits are reserved for the transparency of each pixel (alpha). This pixel format is the standard format on Android (called RGBA8888), but this hasn’t always been the case. As it happens, for Android implementations up to version 2.x or thereabouts, the standard pixel format was RGB565, which means 5 bits for red, 6 for green, 5 for blue and no alpha. This format does not support transparency.
When you call
Canvas.drawBitmap, the bitmap you pass to it needs to be converted to the right color format before it can be drawn on the target canvas. If you’re lucky, the target canvas’s color format is the same as your bitmap’s, which will make the process fast. Also, in older Android versions, if you needed transparency, you would use the RGBA4444 format, which uses less bits per pixel and was therefore faster to draw. This format is now deprecated. You’ll find some answers on StackOverflow mentioning that you can speed up drawing transparent images by using this format but sadly you can’t anymore.
As a rule of thumb, most Android devices that you’ll encounter today use the RGBA8888 format by default (my Samsung Galaxy tablet still uses RGB565, but it’s 3-4 years old), so that’s the format you should use for your images. If needed, you can force your bitmaps into this format. When you implement prescaling, this would be a good moment to do just that, because you’ll need to create a fresh bitmap to scale into anyway.
A thing often overlooked by people new to Android (and maybe Java) is the fact that garbage collection can ruin your day. If a garbage collection is started in the middle of your drawing loop, your frame rate will drop fast. Therefore:
- Do not read any bitmaps inside your drawing loop. Create, read (and prescale) them in your initialization procedure.
- Do not create any
Paintobjects inside your drawing loop. Create all
Paintinstances you will need beforehand, any only manipulate them (as little as possible) in your drawing code.
- Do not create any
Stringinstances inside your drawing loop.
Actually, this can be summarized as “do not create anything inside your drawing loop”. Or: “the keyword
new must not appear in your drawing loop”.
String instances, in Java they are a dangerous thing. Each time you say something like:
…you’re creating a new
String instance in memory which will eventually be destroyed by the garbage collector. At 60fps, this will happen a lot. You can monitor your code using the Android Device Monitor; if you create any strings inside your drawing loop you’ll see lots of garbage collections, and that’s bad.
Now for the bad news: you cannot use alpha compositing. That is, you cannot draw 32-bits transparent bitmaps over existing bitmaps. It’s not that it doesn’t work, but it’s very slow. For every source bitmap pixel plotted, Android has to access a pixel from the destination bitmap and perform some calculations. In the case of a non-transparent image in RGB565 format, this isn’t necessary and Android can straight
memcopy the source bitmap to the target bitmap (except where
PixelFormat conversions apply). In fact, for RGBA8888 to RGBA8888 bitmap drawing, you can still get high speeds if you turn transparency off. You can do so by using a Porter-Duff filter of SRC or DST. However, none of this solves the problem: transparency is slow and you will need transparency.
For your game background screen at least, do not use a transparent image. Use the RGB565 format (it will save some memory) or RGBA8888 with a Porter-Duff filter set to no transparency.
On the upside: in my experience, the alpha compositing problem isn’t noticeable on small devices. Tests on phones with 800×480 and 320×480 resolutions yielded frame rates of 60 fps, even while filling the whole screen three times over with transparent images. However, on 1280×800 (a Samsung tablet), the frame rate dropped to 30 fps, while on a 1980×1024 Lenovo phone it went down to about 15 fps. It seems that canvas drawing, although reportedly reimplemented in recent Androids to take advantage of hardware acceleration, does not perform well drawing transparent images on larger screens (with more powerful GPUs presumably).
This post ends with a bit of a downer, but don’t despair. If you have lots of transparent images and a low frame rate (and which remains low in spite of the tips above), then consider OpenGL. It’s somewhat more difficult to implement, but if you stick with 2D it isn’t too hard. What is more, frame rates go up considerably in spite of all the transparent images. Filling the screen seven times over with full-screen transparent images, I obtained a framerate of at least 23 fps on all devices (more on some). For a more real-world scenario of filling the screen 3 times over, I got 60 fps on small phone screens and the Samsung Galaxy Tab (1280×800). On the Lenovo P1c72, which its huge 1920×1080 screen, I still got 52 fps.