Tuesday, November 29, 2011

Optimizing View Hierarchies

This blog post is a brain dump of my findings discovered while optimizing a ListView in Android. It covers ListView best practices, including the viewholder pattern, ListView row type, ListView footer. It also shows how to use layoutopt and hierarchyviewer from the Android Developer Tools (ADT) to help optimize your layouts.

I noticed that the scrolling in my ListView activity had some performance issues in regards to scrolling. There’s not a lot of complex stuff going on visually in the rows so scrolling should not really be an issue.  We already follow the ViewHolder pattern, where we take advantage of automatic ListView row recycling by the OS and make sure to not call findViewById on every view we manipulate and every call to getView() in the adapter. So it seemed like something else is the culprit. 

To help discover the problem, I decided to go under the hood and get a more tangible look at view hierarchy using hierarchyviewer from ADT. Note, if you have not used hierarchyviewer before, it is very useful in debugging and profiling your UI. Luckily, it happens to be easy to use, simply open a shell and run the hierarchyviewer command. A gui will launch that allows you to examine your application process and a tree of its views. It must be noted, that you android:debuggable attribute must be set to true in the manifest, and that hierarchyviewer can only access hierarchies from devices running a developer version of the OS, such as an emulator or a Nexus phone. This is what the view hierarchy for a single row of the ListView looked like:

I noticed that there was much cruft in the layout row file for the ListView. After investigation, it seemed that there could be a few improvements done to reduce this view hierarchy to something much more reasonable for the amount of data that is actually being shown. The real problem is that we were using the ListView improperly for our app’s functionality. Our ListView shows a basic layout for every row of the list, and expands to show a larger and more information-rich view when selected. As you can see from the hierarchy above, there are two main branches. The branch on the top contains the views for the expanded, special row. Our adapter contained logic to specify the visibility of the expanded views accordingly. This logic gets the job done, but a more lean and appropriate approach leverages the row type facility built in to Adapters.

By overriding getItemViewType() and getItemViewTypeCount() in my Adapter implementation, I was able to inflate and return different xml layouts conditionally. This effectively removed the top branch from the view hierarchy. Instead of having 12 layouts and 19 views in the first implementation, the hierarchy was reduced to 5 layouts and 8 views. Here is the improved version under hierarchyviewer:

The layout has improved, but it looks like improvements can still be made. The next improvement was implemented using knowledge of the power and ability of ListViews. I noticed that there was an extra padding view present in every row object. You may have run in to this problem before with ListViews: designer creates a slick mock for a list, and even provides the assets for the background the row. The background asset contains padding on only the top. This allows for nice, even spacing when the rows are lined up, one after the other. But, you still want there to be padding on the bottom after the list item (note: there can be a number of variations on this problem, but I have seemed to have seen this problem every time I work with a making a pretty ListView). This padding has to be added manually somehow. Originally this problem was being solved by including an extra view in the row, and then setting the visibility to VISIBLE if it happens to be the last row. Unfortunately, there are two horrible problem with this solution: 1. There include extra logic in the getView() method that does not need to be there. 2. This solution introduces an extraneous view, and level to the hierarchy to support it.

My solution to this problem leverages the footer facility of the ListView. In this manner, I have removed the padding View, and its containing layout from the row, and instead replaced it with a footer, which attaches to the bottom of the list and scrolls with it, boiling down to the same functionality. Now the hierarchy looks like this:

Wow! What an improvement. There are now only 5 levels, 4 layouts, and 7 views. It is looking pretty good, although, it looks like one more minor improvement can be made. The LinearLayout in the middle of the hierarchy does not look like it is doing much. Lets use layoutopt to confirm this fact. It seems that layoutopt is more of a proactive tool, telling me the user directly what is wrong, whereas hierarchyviewer is more a complementary tool to use while debugging that visualizes the views. To use layouopt, simply jump in the shell and run layoutopt with an argument of the file name. It spits out some useful information which confirm that this LinearLayout is useless, so I went ahead and removed it. Now, after all of the optimizations, it has only 4 levels, 3 layouts, and 7 views. The layout looks like this:

Now you are probably wondering why I went to all of this trouble just to reduce the view hierarchy. These are the two very important reasons I decided to go to this trouble:
  • Improved performance. Snappier app means happier customer. Reducing the hierarchy for most activities may not be the most beneficial way to spend your time, but the dividends are much more apparent in a row for a ListView. An optimized layout will directly affect the scrollability and usability of the list.
  • Cleaner code. By using built in ListView facilities, such as row view type and footers, the logic pertaining to the ListView became much cleaner. This means it will be easier to maintain, especially as new developers are brought on to work on the project.
I suppose the moral of this post it to make sure that you are properly using the API and tools given to you in effort to reduce to decrease headaches. And don’t be afraid to refactor!

Are you suffering from any of these pitfalls? I suggest running your application in hierarchyviewer to get a visualization of your views, especially if you have a ListView. You may be surprised at what you see!