Android & Kotlin Tutorials

Learn Android development in Kotlin, from beginner to advanced.

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.

5/5 4 Ratings

Version

  • Kotlin 1.3, Android 4.4, Android Studio 3.4

The Android Memory Profiler is a tool which helps you understand the memory usage of your app. Every Android developer should understand memory management. Memory pitfalls cause many of the crashes and performance issues in Android apps.

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

Note: This tutorial assumes you have previous experience developing for Android in Kotlin. If you’re unfamiliar with the language, have a look at this tutorial. If you’re beginning with Android, check out some of our Getting Started and other Android tutorials.

Android Profiler

Android Profiler, which replaces Android Monitor tools, is included in Android Studio 3.0 and later. It measures several performance aspects of an app in real-time like:

  • Battery
  • Network
  • CPU
  • Memory

In this tutorial you’ll focus on memory analysis.

Android Memory Management

The Android virtual machine keeps track of each memory allocation in the heap. The heap is a chunk of memory where the system allocates Java/Kotlin objects.

There’s a process for reclaiming unused memory known as garbage collection. It has the following objectives:

  • Find objects that nobody needs.
  • Reclaim the memory used by those objects and return it to the heap.

You don’t generally request a garbage collection. Instead, the system has a running set of criteria to determine when to perform one.

To enable a multi-task environment, Android puts a limit on the heap size for each app. This size will vary depending on how much available RAM the device has. When the heap capacity is full and the system tries to allocate more memory, you could get an OutOfMemoryError.

Garbage Collection Roots

Suppose you have the following objects in memory:

gc_roots

The top white objects are called GC Roots. No other object in the heap references them.

To simplify things, you can think of obj9 as an activity retaining other objects: obj10, obj11 and obj12.

Suppose you’re in that activity and press back, finishing it. The system will clear the reference from obj7 to obj9:

gc roots obj7 clears reference

When the system triggers the Garbage Collector, it’ll start from the GC roots. It’ll realize that obj9 and the rest of the objects retained by it, obj10, obj11 and obj12, aren’t reachable and will collect them.

Why You Should Profile Your App Memory

The system has to pause your app’s code to let the Garbage Collector do its job. Usually, this process is imperceivable.

But other times you’ll notice your app is sluggish and skipping frames. This happens when your app allocates memory faster than the system can collect it.

When you leak memory it can’t be released back to the heap. This forces unnecessary garbage collection events and slows the rest of the system. Eventually, the system may kill your app process to reclaim the memory.

To avoid these problems, you should profile your app memory.

Getting Started

To get started with this tutorial, click the Download Materials button at the top or bottom of the tutorial to download the starter project.

Throughout this tutorial you’ll work with TripLog app. TripLog lets the user write notes about what they are doing and feeling during a trip.

Open Android Studio 3.4.1 or later, click File ▸ New ▸ Import Project. Select the top-level project folder for the starter project you downloaded.

Alternatively, you can select Open an existing Android Studio project from the Welcome screen. Again, choose the top-level project folder for the starter project you downloaded.

Build and run TripLog to become familiar with it:

trip log app main screen trip log app detail screen

The TripLog project contains the following main files:

  • MainActivity.kt contains the main screen. It’ll show all the logs here.
  • DetailActivity.kt allows the user to create or view a log.
  • MainApplication.kt provides dependencies for the activities: a repository and formatters.
  • TripLog.kt represents the data of a log.
  • Repository.kt saves and retrieves logs.
  • DateFormatter.kt formats the data to show in the screens.

Using the Android Profiler

Open the Android Profiler:

android profiler button

You can also open it by going to View ‣ Tool Windows ‣ Profiler.

Note: Alternatively, you can build and directly profile the app by going to Run ‣ Profile ‘app’ or pressing the Profile button in the Navigation Bar.

You should see the current session:

android memory profiler

If you don’t see the session listed, press the + button and select the app.

android memory profiler add session

Note: If you’re using a device or emulator running Android 7.1 or lower, you’ll see a message stating, Advanced profiling is unavailable for the selected process. To enable it, go to Run ‣ Edit Configurations and select your app in the left pane. Then go to the Profiling tab. Finally, check Enable advanced profiling.
Apply, build and run your app again.

Now, click the Memory section:

android memory profiler memory

At the top, you’ll see the current activity. In this case it’s the MainActivity.

In the app, press the + button to add a new log and check the profiler:

android memory profiler detail activity

It now shows the DetailActivity is the current activity and a small pink dot. The profiler shows these dots each time you touch the screen. Later, you’ll also see other icons, such as back or keyboard, when you press them on your device or emulator.

Below that, you can see the memory count of your app, segmented into several categories:

  • Java: Memory from objects that Java/Kotlin has allocated.
  • Native: Allocated memory from C/C++ code objects.
  • Graphics: Memory to display pixels to the screen.
  • Stack: Memory used by both native and Java stacks in your app. When your app invokes a method, a block is created in the stack memory to hold local primitive values and references to other objects in this method.
  • Code: Memory used for code and resources such as dex bytecode, .so libraries and fonts.
  • Others: Memory that the system doesn’t know how to categorize.
  • Allocated: The number of Java/Kotlin objects your app has allocated.
Note: If you’re running a device or emulator with Android 7.1 or lower, this allocation count starts only at the time the Memory Profiler connected to your running app. It doesn’t count any objects allocated before you started profiling.

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:

android memory profiler triplog first alloc android memory profiler triplog second alloc

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

Challenge

Throughout the tutorial you’ve used an in memory repository to get and save the logs. Therefore the logs are always deleted between sessions of the app.

Open MainApplication.kt and you’ll see a commented line to use a SharedPreferences repository implementation that permanently saves the logs. Uncomment this line and comment or delete the one you’ve been using.

Build and profile the app to see it’s creating many TripLog instances. For example, after adding five logs you’ll see the app actually allocated eleven instances. As a challenge, try to refactor SharedPreferencesRepositoryImpl to minimize the quantity of allocations.

You can find the completed challenge in the same download from the Download materials button found at the top or bottom of this tutorial.

Where To Go From Here?

You can download the completed project using the Download materials button at the top or bottom of the tutorial.

Congratulations! You now know how to:

  • Use the Android Profiler to analyze memory.
  • Use Heap Dumps to identify which classes allocate large amounts of memory.
  • Perform Allocation Tracking over time to understand when the app allocates or deallocates objects and also to know where in your code the allocation is happening.

If you want to learn more about the subject, please check the following references:

I hope you enjoyed this introduction to Android Memory Profiler tutorial. If you have any questions, comments or awesome modifications to this project app please join the forum discussion and comment below!

Average Rating

5/5

Add a rating for this content

4 ratings

Contributors

Comments