While testing our android app on tablets a while ago, a developer reported that the app had a glitch on the Search Results in landscape mode. After making a search, when we scroll on the search results page, the screen becomes unresponsive.
We immediately started investigation and were easily able to reproduce this behavior on multiple real devices as well as on emulators. So it was not device specific and the amount of memory on the device was for sure not a concern.
The first logical step that came to our mind was to profile and see if we can spot a bottleneck in the CPU Usage or Memory Usage monitors.
While memory profiler showed us a lot of things that we were tempted to look into, one of the devs said we must first look at the CPU usage because it seems some heavy computation is happening on the main thread that is rendering the app completely unusable. We moved to the CPU usage flame chart in unison.
Here we found out that the
RecyclerView was generating more
Views than needed. It was then that we figured, for the initial load of the screen, we need 5 different types of views which the
RecyclerView might want to already create and cache. What we saw didn’t look very nice. Here is the picture of the Call chart.
The first blue colored call that you see when viewing from top to bottom is the call to
onCreateViewHolder method. Something kept on triggering the call to this method infinitely.
We started playing with
RecyclerView API to control view cache size but to no avail.
We also tried to look into our
ViewHolder to find out if “maybe” we were doing something nasty there but couldn’t find anything that contributed to this problem.
While reading the code in the fragment, we stumbled upon an issue reported on Google, titled
RecyclerView notifyItemChanged Prevent Scroll.
Here is the code snippet which led us to that link:
recyclerView.setHasFixedSize(true); // In tablet landscape mode the RecyclerView inflated all views because of the bug : // This was also cause by the upgrade to support library 23.3 and 23.4 // If this bug is fixed by Google we might use the default for setAutoMeasureEnabled to true again if that is better. srLayoutManager.setAutoMeasureEnabled(false);
Apparently, due to some changes in the past releases of
RecyclerView something changed again and even, this fix stopped working.
We then went to the XML file of the
Activity in which the two fragments were being loaded on the tablet.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <FrameLayout android:id="@+id/searchresults_list" android:layout_width="0px" android:layout_height="match_parent" android:layout_weight="1"/> <View android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/bui_color_grayscale_light"/> <FrameLayout android:id="@+id/searchresults_map" android:layout_width="0px" android:layout_height="match_parent" android:layout_weight="1"/> </LinearLayout>
A lot of you might have understood the problem by now, right? But I’ll still continue for those who haven’t.
RecyclerView’s parent was measuring it with unlimited
height spec. So the parent was literally asking the
RecyclerView to layout as many items as it can, which it did – well, at least tried to.
The problem is that we were using weighted width on a horizontal weighted linear layout. To calculate the weight distribution, it measured both children with unlimited space, then distributed the remaining space.Inferred from Official Android Documentation
Even though our
MATCH_PARENTbecause it was a horizontal linear layout and baseline align is set to true (by default, it is true),
LinearLayouttried to measure children’s height to be able to align them, thus, measuring the child with unlimited height.
This is not really a bug in the
LinearLayout could be more clever not to do this when the child is match_parent but since we were setting baseline align in match_parent children, it was also inconsistent.
We wrote another variant of the same screen using
RelativeLayout which fixed the issue for us.
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <FrameLayout android:id="@+id/searchresults_list" android:layout_alignParentStart="true" android:layout_toStartOf="@+id/searchresults_tablet_divider" android:layout_width="match_parent" android:layout_height="wrap_content"/> <View android:id="@+id/searchresults_tablet_divider" android:layout_width="1dp" android:layout_height="match_parent" android:layout_centerInParent="true" android:background="@color/bui_color_grayscale_light"/> <FrameLayout android:id="@+id/searchresults_map" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_toEndOf="@+id/searchresults_tablet_divider"/> </RelativeLayout>
Here is the call chart we saw while running the app with the new variant of the Layout.
The number of calls to
onCreateViewHolder reduced significantly and
RecyclerView only drew what we expected it to.
This is how we solved this major bottleneck in the tablet version of the app.
In the end, I’d like to conclude that the Android Studio Monitors for Profiling CPU and Memory Usage can be very handy. Never underestimate the power of profiling your code, you’ll always find something to improve there. But be careful as it can be very tempting to fall into traps of code that doesn’t really need optimization.