Tags

, , , , , , , , , , , , , , , , , , , , , , ,

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 (iv) Ideal condition

(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.

stacked-layers

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!

About these ads