Android Memory Profiler: Getting Started

In this Android Memory Profiler tutorial, you’ll learn how to track memory allocation and create heap dumps using the Android Profiler. By Fernando Sproviero.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Allocation Tracking

You’ll now see how to analyze when TripLog objects are created in memory.

Enter something into the What are you doing? field. Press the check button on the top right corner to save it.

In the Memory Profiler, if you’re running a device with Android 8.0 or higher, drag in the timeline to select the region between the DetailActivity and the MainActivity.

android profiler allocation tracking

Note: If you’re running a device with a lower version, before creating the log you’ll need to click on Record memory allocations. Then add the log and press Stop recording.

Below the timeline, the profiler displays the Live Allocation results:

android memory profiler allocation list

This shows the following for each class:

  • Allocations: Number of objects allocated in this period of time.
  • Deallocations: Quantity of deallocations in this period of time.
  • Total Count: Number of objects still allocated of this class.
  • Shallow Size: Total bytes of memory used for the objects of this class.

Filter by TripLog, matching case, and you’ll see the following:

android memory profiler triplog filter

While you probably didn’t expect it, there are two allocations of TripLog:

android memory profiler triplog alloc

To find out why, click on each row of the Instance View to see more information about each allocation:

In the Allocation Call Stack, one TripLog instance was allocated in the onOptionsItemSelected() method. This was called when you pressed the check button on the top right corner.

The other one was created in onActivityResult() of MainActivity as a result of the unparcel.

In the app, delete the log by pressing the trash button on the top right corner of the main screen.

In the Memory Profiler, scroll the timeline until the moment you pressed the button. Perform an allocation tracking by dragging the timeline in to see if the TripLog instances were deallocated when you pressed that button.

Note: If you’re using Android 7.1 or lower, first click on Record memory allocations. Then delete the log and press Stop recording.

android memory profiler trash no deallocs

There were no deallocations! Are you confused?

confused

This is because the Garbage Collector didn’t pass yet. Therefore there are no deallocations.

To force a garbage collection, press the following button two times:

android memory profiler force gc

Perform an allocation tracking by dragging the timeline in to see that the instances were finally deallocated.

android memory profiler deallocs

happy

Heap Dump

You can also analyze the current state of the memory by performing a heap dump.

You’ll simulate that you have 100 logs. There’s a hidden button to do so. Open activity_main.xml and change the visibility of the button with buttonAddMultipleLogs id to visible.

Go to Run ‣ Profile ‘app’ and press the Add 100 logs button. After a few seconds, you’ll notice the logs were added to the list.

Now, in the Memory Profiler press the Dump Java heap button:

android memory profiler dump heap

Below the timeline you’ll see the heap dump:

android memory profiler dump result

This shows the following for each class:

  • Allocations: Quantity of allocations.
  • Native Size: Total bytes of native memory.
  • Shallow Size: Total bytes of Java memory.
  • Retained Size: Total bytes being retained due to all instances of this class.

Order by Retained Size to see that there were exactly 100 ConstraintLayout objects allocated. Continue adding logs and capturing heap dumps. You’ll see that this number increases by exactly the number of logs you add.

This means that the app is creating one ConstraintLayout per TripLog. Open MainActivity.kt and check the refreshLogs() method.

  private fun refreshLogs() {
    layoutContainer.removeAllViews()
    repository.getLogs().forEach { tripLog ->
      val child = inflateLogViewItem(tripLog)
      layoutContainer.addView(child)
    }
  }

  private fun inflateLogViewItem(tripLog: TripLog): View? {
    return layoutInflater.inflate(R.layout.view_log_item, null, false).apply {
      textViewLog.text = tripLog.log
      textViewDate.text = dateFormatter.format(tripLog.date)
      textViewLocation.text = coordinatesFormatter.format(tripLog.coordinates)
      setOnClickListener {
        showDetailLog(tripLog)
      }
    }
  }

By reading this code you can confirm that the app is inflating one view per log. If you open view_log_item.xml you’ll see it’s a ConstrainLayout.

This isn’t good because it’s not recycling the views. That could eventually create an OutOfMemoryError and generate a crash in your app. The solution is to refactor the app using RecyclerView.

Open activity_main.xml and replace the ScrollView with the following:

  <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    tools:itemCount="3"
    tools:listitem="@layout/view_log_item" />

Open MainActivity.kt, delete the refreshLogs() and inflateLogViewItem() methods. Paste the following:

  private fun refreshLogs() {
    val adapter = TripLogAdapter(this, repository.getLogs(), 
      dateFormatter, coordinatesFormatter)
    adapter.listener = this
    recyclerView.adapter = adapter
  }

Let the MainActivity implement the following listener:

class MainActivity : BaseActivity(), TripLogAdapter.Listener {

And also change the following:

private fun showDetailLog(tripLog: TripLog) {

To this:

override fun showDetailLog(tripLog: TripLog) {

This won’t compile yet. So, create a new file called TripLogAdapter.kt with the following content:

class TripLogAdapter(context: Context,
                     private val logs: List<TripLog>,
                     private val dateFormatter: DateFormatter,
                     private val coordinatesFormatter: CoordinatesFormatter
) : RecyclerView.Adapter<TripLogAdapter.TripLogViewHolder>() {

  var listener: Listener? = null

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
      : TripLogViewHolder {
    val inflater = LayoutInflater.from(parent.context)
    val itemView = inflater.inflate(R.layout.view_log_item, parent, false)
    return TripLogViewHolder(itemView)
  }

  override fun getItemCount() = logs.size

  override fun onBindViewHolder(holder: TripLogViewHolder, position: Int) {
    val tripLog = logs[position]
    holder.bind(tripLog)
  }

  inner class TripLogViewHolder(itemView: View) 
      : RecyclerView.ViewHolder(itemView) {
    private val textViewLog = itemView.textViewLog
    private val textViewDate = itemView.textViewDate
    private val textViewLocation = itemView.textViewLocation

    fun bind(tripLog: TripLog) {
      textViewLog.text = tripLog.log
      textViewDate.text = dateFormatter.format(tripLog.date)
      textViewLocation.text = coordinatesFormatter.format(tripLog.coordinates)
      itemView.setOnClickListener {
        listener?.showDetailLog(tripLog)
      }
    }

  }

  interface Listener {
    fun showDetailLog(tripLog: TripLog)
  }

}

Profile the app again and see that now it creates fewer ConstraintLayout objects because it’s recycling the views.

Frequent Garbage Collection

Garbage Collection, also called memory churn, happens when the app allocates but also has to deallocate objects in a short period of time.

For example, it can happen if you allocate heavy objects in loops. Inside each iteration, the app has to not only allocate a big object, but also deallocate it from the previous iteration so it doesn’t run out of memory.

The user will notice stuttering in the app because of frequent garbage collection. This leads to a poor user experience.

To see this in action, add a toggle button when writing your log so that the user can also describe her or his mood. So, open activity_detail.xml and add the following above the EditText:

  <ToggleButton
    android:id="@+id/toggleButtonMood"
    android:layout_width="50dp"
    android:layout_height="50dp"
    android:layout_gravity="center"
    android:background="@drawable/bg_selector_mood"
    android:checked="true"
    android:textOff=""
    android:textOn="" />

Open DetailActivity.kt and add the following to the showNewLog() method:

toggleButtonMood.isEnabled = true

Add this to the showLog() method:

toggleButtonMood.isChecked = log.happyMood
toggleButtonMood.isEnabled = false

Finally, update the creation of the TripLog in the newLog() method:

val log = TripLog(editTextLog.text.toString(), Date(), null, 
    toggleButtonMood.isChecked)

Don’t forget to update TripLog.kt:

data class TripLog(val log: String, val date: Date, val coordinates: Coordinates?, val happyMood: Boolean = true) : Parcelable

Now, to see the mood of each log in the main screen you need to update the TripLogAdapter class. So open it and update the TripLogViewHolder(itemView: View) inner class with this:

  inner class TripLogViewHolder(itemView: View) 
    : RecyclerView.ViewHolder(itemView) {
    ...
    private val imageView = itemView.imageView

    fun bind(tripLog: TripLog) {
      val happyBitmap = BitmapFactory.decodeResource(itemView.context.resources, 
          R.drawable.bg_basic_happy_big)
      val sadBitmap = BitmapFactory.decodeResource(itemView.context.resources, 
          R.drawable.bg_basic_sad_big)
      imageView.setImageBitmap(if (tripLog.happyMood) happyBitmap else sadBitmap)
      ...

Build and run the app. Add 100 logs and try to scroll:

angry

This would be the face your users make when they encounter such slow performance! So, you’d better see what’s happening and fix it before shipping the app.

Open the Android Profiler which will start monitoring automatically or add the current session manually as explained before.

One of the first things you’ll notice is the Total memory is higher than before. Also, try to scroll in the app and you’ll see something similar to this:

android memory profiler frequent gc

As you can see, the system triggers the garbage collector frequently. You can also confirm this by doing some allocation tracking in the timeline. You’ll see the quantity of objects being allocated are almost the same as those being deallocated.

This clearly isn’t good. Frequent garbage collections is causing bad performance.

Now, perform a heap dump and you’ll see something like this:

android memory profiler frequent gc heap dump

Here you can find a clue. There are some Bitmap objects retaining a lot of memory.

So, do some allocation tracking again when the logs are created. Filter by Bitmap. You can click on one of them to see where they are being created:

android memory profiler frequent gc bitmap alloc

Double click the bind method in the Allocation Call Stack and you’ll go to the source code:

    fun bind(tripLog: TripLog) {
      val happyBitmap = BitmapFactory.decodeResource(itemView.context.resources, 
          R.drawable.bg_basic_happy_big)
      val sadBitmap = BitmapFactory.decodeResource(itemView.context.resources, 
          R.drawable.bg_basic_sad_big)
      imageView.setImageBitmap(if (tripLog.happyMood) happyBitmap else sadBitmap)
      ...

It seems that these lines are causing the problem. The app is unnecessarily decoding R.drawable.bg_basic_happy_big and R.drawable.bg_basic_sad_big each time you bind a log.

So, remove those lines and modify TripLogAdapter.kt as follows:

class TripLogAdapter(context: Context,
                     private val logs: List<TripLog>,
                     private val dateFormatter: DateFormatter,
                     private val coordinatesFormatter: CoordinatesFormatter
) : RecyclerView.Adapter<TripLogAdapter.TripLogViewHolder>() {

  private val happyBitmap = BitmapFactory.decodeResource(
      context.resources,
      R.drawable.bg_basic_happy_big)

  private val sadBitmap = BitmapFactory.decodeResource(
      context.resources,
      R.drawable.bg_basic_sad_big)

Here, you’re decoding the drawables only once instead of doing it on each bind. There are no more frequent garbage collections because you properly allocated memory for the Bitmap objects.

Build and run the app again, you’ll see the performance was enhanced!

celebrate