3. Testing the effectiveness of different Android Tools in detecting performance issues caused by Expensive on Bind Call in RecyclerView

Abhishek Luthra
9 min readDec 18, 2022

--

3.1 Introduction

Scrolling Lists in Android are normally implemented using the RecyclerView widget in android and are one of the most used android widgets to create the UI. Additionally, UI jank in scrollinglists is one of the most common UI jank issues that we can observe in Android[14].

3.2 Code Implementation of Test

RecyclerView contains two main methods as mentioned below :

  • public VH onCreateViewHolder (ViewGroup parent, int viewType)

This is invoked when a new RecyclerView.ViewHolder of the given type is needed to represent an item.

The new ViewHolder basically represents the new item view that needs to be created which can be created either by inflating an XML layout file or by manually inflating view in the code

  • public void onBindViewHolder (VH holder, int position, List<Object> payloads)

This is called when new data needs to be updated to display new data on item view at a particular position. This bind should ideally take less than 1 ms to run and needs to be very quick. It should only take a POJO class and bind that to the view without much manipulation so that the bind call is quick. This is called multiple times when scrolling in recycler view happens.

In order to test the effectiveness of different UI tools in detecting UI jank caused by expensive onBind call, we deliberately added a delay of 50 ms in onBind by using Thread.sleep() to simulate expensive work happening in the onBind() method. In order to verify that the UI jank is being caused by an expensive onBind() call, we repeat the application of different tools on an optimized onbind call, where we have removed Thread.sleep() statement.

3.2.1 MainActivity.java

package com.example.perforamancetest
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.perforamancetest.data.LocalDataSource
import com.example.performancetest.R
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize data.
val myDataset = LocalDataSource().getMessages(this)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = CustomAdapter(this, myDataset)

// Use this setting to improve performance if you know that changes
// in content do not change the layout size of the RecyclerView
recyclerView.setHasFixedSize(true)
}
}

Code 15. MainActivity.java

3.2.2 activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.perforamancetest.MainActivity"
android:background="#FFFFFF">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager" />
</FrameLayout>

Code 16. activity_main.xml

3.2.3 CustomAdapter.java with expensive onBind method call

package com.example.perforamancetest;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.example.performancetest.R;
import java.util.List;
public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {
private List<String> localDataSet;
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder).
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
public ViewHolder(View view) {
super(view);
// Define click listener for the ViewHolder's View
textView = (TextView) view.findViewById(R.id.item_title);
}
public TextView getTextView() {
return textView;
}
}
/**
* Initialize the dataset of the Adapter.
*
* @param dataSet String[] containing the data to populate views to be used
* by RecyclerView.
*/
public CustomAdapter(Context context, List<String> dataSet) {
localDataSet = dataSet;
}
// Create new views (invoked by the layout manager)
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
// Create a new view, which defines the UI of the list item
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.text_item, viewGroup, false);
return new ViewHolder(view);
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Get element from your dataset at this position and replace the
// contents of the view with that element
viewHolder.getTextView().setText(localDataSet.get(position));
}
// Return the size of your dataset (invoked by the layout manager)
@Override
public int getItemCount() {
return localDataSet.size();
}
}

Code 17. CustomAdapter.java with expensive onBind call

3.2.4 CustomAdapter.java with optimized onBind method call

package com.example.perforamancetest;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.example.performancetest.R;
import java.util.List;
public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {
private List<String> localDataSet;
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder).
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
public ViewHolder(View view) {
super(view);
// Define click listener for the ViewHolder's View
textView = (TextView) view.findViewById(R.id.item_title);
}
public TextView getTextView() {
return textView;
}
}
/**
* Initialize the dataset of the Adapter.
*
* @param dataSet String[] containing the data to populate views to be used
* by RecyclerView.
*/
public CustomAdapter(Context context, List<String> dataSet) {
localDataSet = dataSet;
}
// Create new views (invoked by the layout manager)
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
// Create a new view, which defines the UI of the list item
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.text_item, viewGroup, false);
return new ViewHolder(view);
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
// try {
// Thread.sleep(50);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// Get element from your dataset at this position and replace the
// contents of the view with that element
viewHolder.getTextView().setText(localDataSet.get(position));
}
// Return the size of your dataset (invoked by the layout manager)
@Override
public int getItemCount() {
return localDataSet.size();
}
}

Code 18. CustomeAdapter.java with optimized onBind call

3.2.5 text_item.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
/>

Code 19. text_item.xml

3.3 Tool Test

3.3.1 LayoutInspector Tool Test

3.3.1.1 LayoutInspector on RecyclerView with expensive onBind call

Figure 73. LayoutInspector result for MainActivity.java with unoptimized onBind call

3.3.1.2 LayoutInspector on RecyclerView with optimized onBind call

Figure 74. LayoutInspector result for MainActivity with optimized onBind call

3.3.1.3 LayoutInspector Tool Test Result

It is clearly visible from the component tree of Figure 73 and Figure 74 that the layout hierarchy for FibonacciActivity remains the same irrespective of whether Fibonacci is being computed on the main thread or a separate thread.

This indicates LayoutInspector is ineffective in detecting any UI jank due to heavy computation on the main thread

3.3.2 Debug GPU Overdraw Tool Test

3.3.2.1 Debug GPU Overdraw on RecyclerView with unoptimized onBind call

Figure 75. Debug GPU overdraw result for MainActivity with unoptimized onBind call

3.3.2.2 Debug GPU Overdraw on RecyclerView with optimized onBind call

Figure 76. Debug GPU overdraw result for MainActivity with optimized onBind call

3.3.2.3 Debug GPU Overdraw Tool Test Result

It is clearly visible from the overdraw areas of MainActivity with an unoptimized onBind recycler view call(Figure 75) and with an optimized onBind recycler view call(Figure 76) that there is no difference in output given by Debug GPU overdraw developer option.

3.3.3 Profile GPU rendering Tool Test

3.3.3.1 Profile GPU rendering on RecyclerView with unoptimized onBind call

Figure 77. Profile GPU rendering result for MainActivity with unoptimized onBind reyclerview call

3.3.3.2 Profile GPU rendering on RecyclerView with optimized onBind call

Figure 78. Profile GPU rendering result for MainActivity with optimized onBind call

3.3.2.3 Profil GPU rendering Tool Test Result

It is clearly visible from the rendering bars of Main Activity with unoptimized onBind call ( Figure 77) and optimized onBind call(Figure 78) that the rendering bars with optimized onBind recyclerview call are smaller in height than rendering bars with unoptimized onBind recyclerview call. This indicates that the profile GPU rendering is effective in detecting the rendering performance issues caused by expensive onBind recyclerview call.

3.3.4 Show View Updates Tool Test

3.3.4.1 Show View Updates on RecyclerView with unoptimized onBind call

The result of applying Show View Updates developer option on FibonacciActivity with Fibonacci Computation on main thread is shown in Reference [23]

3.3.4.2 Show View Updates on RecyclerView with optimized onBind call

The result of applying Show View Updates developer option on FibonacciActivity with Fibonacci Computation on separate thread is shown in Reference [24]

3.3.4.3 Show View Updates Tool Test Result

It is clearly visible from the video result for Main Activity with expensive onBind recyclerview call[23] and optimized onBind recyclerview call[24] that there is significant difference in results of applying this tool. The view updates slow down when scrolling is done on recyclerview with unoptimized onBind call. This indicates that the Show view updates tool is effective in detecting the rendering performance issues caused by expensive onBind calls in recyclerview .

3.3.5 Android CPU Profiler Tool Test

As part of this test, we will scroll the recyclerview till mid of second item in both the cases: with thread sleep statement in onBind call and without thread sleep statement in on Bind call. The state at the end of the test looks like

Figure 79. The end state of scroll state on recyclerview to test expensive onBind call.

3.3.5.1 Android CPU Profiler tool test for expensive onBind call in recyclerview
3.3.5.1.1 System Trace CPU Profiler output for unoptimized onBind call in recyclerview

Figure 80. The CPU profiler system trace visualization for expensive onBind call in recyclerview

Figure 80 presents three major jank frames during recycler scroll. In order to inspect further, we explore all frame track for systrace in CPU profiler in figure 80

Figure 81. The CPU profiler system trace — All Frame visualization for scrolling on recyclerview with expensive onBind call

Figure 82. The CPU profiler system trace — Janky Frame visualization for scrolling on recyclerview with expensive onBind call.

In Figure 82, it can be observed three jank frames during scrolling on recyclerview.

3.3.5.2 Android CPU Profiler tool test for optimized onBind call in recyclerview
3.3.5.2.1 System Trace

Figure 83. The CPU profiler system trace visualization for recyclerview scroll with optimized onBind call

Figure 84. The CPU profiler system trace — All Frame visualization for recyclerview scroll with optimized onBind call. This window shows all frames created in the current system trace recording.

Figure 85. The CPU profiler system trace — Janky Frame visualization for recyclerview scroll with optimized onBind call. This window shows none of the frames is janky.

From Figure 85, we see that there is no UI jank when scrolling is done on recyclerview with optimized onBind call.

3.3.5.3 Android CPU Profiler Tool Test Result

It is clearly visible from the CPU profiler result for expensive and optimized onBind call in recyclerview scroll that there is a significant reduction in the number of jank frames itself by using optimizing the onBind call to take less time. This indicates that the CPU profiler tool is effective in detecting the rendering performance issues caused by running expensive operations in onBind call in recyclerview scroll.

This clearly shows that there is a 100 percent improvement in the number of jank frames by using a separate thread for heavy computation tasks rather than running on the main thread.

3.3.6 Tool Test Results on unoptimized onBind call in recyclerview

Tool used

Can detect rendering issues caused by unoptimized on bind call in recyclerview ?

If it is known that there is some UI jank or rendering performance issue, can this tool attribute the performance issue to unoptimized on bind call in recyclerview?

--

--