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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android Memory Profiler: Getting Started
20 mins
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.
Below the timeline, the profiler displays the Live Allocation results:
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:
While you probably didn’t expect it, there are two allocations of TripLog
:
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.
There were no deallocations! Are you 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:
Perform an allocation tracking by dragging the timeline in to see that the instances were finally deallocated.
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:
Below the timeline you’ll see the heap dump:
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:
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:
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:
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:
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!