Tags
android, background, custom-ui, draw, drawable, gpu, gpu overdraw, hidden, hidden view, imageview, layer, mozilla, optimization, optimize, overdraw, paint, performance, reduce, surfaceview, textview, thumbnail, visible, window, window background
A couple of months back Romain Guy had posted a blog on performance of an Android app. Of the many things, overdraw seemed to be more easier to see instantly. I enabled the Show GPU Overdraw option in Developer Options and launched Fennec. Though some red areas were expected, the result was a shock. Most parts of the app was covered in red. Android gave some consolation in leaving a small portion in light red.

(i) Actual UI, (ii) Overdraw to start with, (iii) Actual reduction in overdraw, and (iv) Ideal condition
After understanding how overdraw is calculated, and how the problems can be mitigated, I started working on it. Basically there doesn’t seem to be a model that calculates what area of a view has to be repainted on the screen at any given time. In other words, Android doesn’t ask the view to paint only the region that is actually seen by the user. Instead the entire view is repainted if it is has its visibility as View.VISIBLE. To think in terms of windowing systems, a background application’s paint can be avoided when the foreground application occupies the entire screen. However, in case of Android, it paints both the hidden and visible views at every frame. By hidden the views are still View.VISIBLE
, but not visible to the user. How do we solve this problem? To solve the problem, we need to see how complex the painting of a thumbnail in the screen was.
The actual thumbnail was the sixth layer drawn on the screen! The thumbnail had a text draw over it, and it by itself had a background which is usually hidden from the user. The about:home had a blue background, so did the Window
. The tabs UI had a darker background drawn even though it was hidden, ummm, from the user’s eyes. There was also a SurfaceView
, used for showing the HTML page, with a white background.
Android highly recommends using a background color for the Window. But why is it useful? 1. It makes the app feels like it loaded right when the user taps on the icon in the launcher, and 2. it works the same as a HTML background for the page. So, it’s good to have the window background and take the strain out of individual background in the app. For example, a white background for Facebook app’s window, instead of individual rows is better. But Firefox for Android has a different problem. We support themes starting version 19. If we had to set the image from the theme as the background of the window, then the about:home and url-bar over it wouldn’t have a background, but instead scroll over it. Also, hiding SurfaceView whenever about:home is shown will become a mess. But, having a background for Window and about:home is a clear overdraw! The way to solve this is to set a Window background at startup, and remove it after the UI is shown to the user. The app will be perceived as having launched faster, but the overdraw is removed for the lifetime of the app.
@Override public void onWindowFocusChanged(boolean hasFocus) { if (hasFocus) getWindow().setBackgroundDrawable(null); }
That removes one layer out of the equation. Let’s take the case of tabs UI. When the app launches, it doesn’t open the tabs right away. The UI for tabs is hidden until the user hits the show-tabs button. This means that there is no point is having the UI with a state View.VISIBLE
when the user does not even see it. The UI can be made View.VISIBLE
right when the user taps on the tabs-button, and made View.INVISIBLE
when it is hidden. As a matter of fact View.GONE
could also be used. However, since there is a small animation involved, to know the height of the tabs UI, we had to use View.INVISIBLE
. This ensures that the View is not drawn, but onLayout()
is called to know the width, the height and the position of the View. A simple change in XML removes the next layer from the stack.
SurfaceView
has a interesting property. The View wouldn’t be created and initialized if its not View.VISIBLE
. Here comes the actual vs. ideal scenario in the first image. Ideally, if the same technique for Tabs UI is applied for the SurfaceView, the overdraw would be reduced by two times. However, since the code to handle it becomes messier, we avoided doing the same.
The last part in the stack involves the thumbnail. As mentioned earlier, thumbnail has an image of its own and a background. The background (30% white) is used in cases where a thumbnail image is not available. However, we keep drawing the background even when the image occupies the entire width and height of the ImageView
. How about clearing the background if there is an image available? That would reduce one more layer!
// somewhere in the code if (thumbnail == null) { thumbnailView.setImageBitmap(R.drawable.some_default_image); thumbnailView.setBackgroundColor(0x22FFFFFF); } else { thumbnailView.setImageBitmap(thumbnail); thumbnailView.setBackgroundColor(0x0); }
Though this might seem a silly optimization, this would benefit a lot of apps that requires loading images from a server. How do we deal with the TextView
? As per the design, we wanted our TextView to be shown over the ImageView. Hence an overdraw couldn’t be avoided. This is what is seen in third screenshot. However, if we could move the TextView to be shown below the ImageView, the amount of overdraw on the TextView remains the same as the ImageView, as in the last screenshot. If we move the TextView down, wouldn’t it use a vertical LinearLayout instead of a RelativeLayout? Yes, it would! And wouldn’t the ImageView be enclosed in a RelativeLayout, that also has another ImageView for the small triangle on right corner? Maybe. And doesn’t this increase the number of views significantly? Of course yes!
<!-- TextView over ImageView --> <RelativeLayout> <ImageView ..thumbnail.. /> <ImageView ..triangle.. /> <TextView/> </RelativeLayout> <!-- TextView below ImageView --> <LinearLayout ..vertical.. > <RelativeLayout> <ImageView ..thumbnail.. /> <ImageView ..triangle.. /> </RelativeLayout> <TextView /> </LinarLayout>
There is a better way to handle this. A custom view can take care of drawing the triangle over the actual image bitmap when needed. This would involve just one View, but two draw operations. This is shown as the green overdraw only for the triangle in the last screenshot. However, if pin’s bitmap can be blended with the image and painted in one draw, the overdraw can be reduced further. It’s just the trade-off between two draws vs. blending images that would win there. A very small overdraw is better than a complex bitmap calculation usually.
So, finally, how many layers do we have in last screenshot? 1. about:home background, 2. thumbnail image. Just two layers! Hence the light blue region instead of bright red!
Nice work! Just a quick note: when Android renders an application using the software pipeline, views hidden by other views are not drawn. It works at invalidation time: when a View invalidates, we traverse the View hierarchy all the way to the root. If we traverse an opaque View along the way, any View under it (i.e. higher in the hierarchy) is marked “dirty opaque,” and will be ignored at draw time.
Because hardware acceleration relies on display lists, we couldn’t use this mechanism. We’re however planning on optimizing away invisible views in the hardware pipeline. No ETA though 🙂
Note that in both cases the optimization works only for Views that return true from View.isOpaque() (the default implementation queries the View background for its opacity, it can be overridden by apps for custom Views.)
I thought the software model would do it. As the older windowing systems used to find the dirty region to paint where there is overlapping. Thanks for clarifying 🙂
Great job! I also remove lots of overdraw issues using Romain’s post about it. Sriram, I have question about some good source of information about rendering/graphics systems used to composing windows. I would like to improve my knowledge about graphics systems/software’s. Any information will be helpful.
Interesting stuff! Do you have any numbers for how much this helped painting performance? How much better would the ideal condition perform compared to what you went with?
I don’t have the numbers as such. For Firefox, about:home is not the primary UI. Hence reducing overdraw is good, but not a must. However, I could say that the scrolling of contents in about:home got much smoother with it.
Great article! I also wrote about Overdraw – finding it and fixing common issues, along other performance optimizations techniques, on one of my blog posts: http://udinic.wordpress.com/2013/03/04/android-app-to-the-challenge/
Navigation drawers also adds an overhead since you need to make your views child of it. Is there a way it can be optimized?
The technique to remove one level of overdraw by setting the window background to null has problems starting from API Level 22.
I haven’t figured out the root cause yet, but using it caused my entire screen to go black(except for the status bar) when my layout had a FrameLayout in it(an empty one for loading a Fragment).
I still achieved the same effect by setting the background to a colour drawable with transparent colour instead.