Memory Leaks in Android
In this Memory Leaks in Android tutorial, you’ll learn how to use the Android Profiler and LeakCanary to detect common leaks and how to avoid them. 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
Memory leaks are a common cause of crashes in Android apps. Every Android developer should understand them and know how to avoid them.
In this tutorial you’ll learn how to use Android Profiler and LeakCanary to detect memory leaks.
Android Profiler and LeakCanary
Android Profiler replaces Android Monitor tools and comes with Android Studio 3.0 and latter. It measures several app performance aspects in real-time like:
- Battery
- Network
- CPU
- Memory
LeakCanary is a library designed to detect memory leaks. You’ll learn what they are, common cases and how to avoid them.
In this tutorial you’ll focus on memory analysis to detect leaks.
Memory Leaks
A memory leak happens when your code allocates memory for an object, but never deallocates it. This can happen for many reasons. You’ll learn these causes later.
No matter the cause, when a memory leak occurs the Garbage Collector thinks an object is still needed because it’s still referenced by other objects. But those references should have cleared.
The memory allocated for leaked objects acts as an immovable block, forcing the rest of the app to run in what’s left of the heap. This can cause frequent garbage collections. If the app continues to leak memory, it’ll eventually run out of it and crash.
Sometimes, a leak is big and obvious. Other times it’s small. Smaller leaks are more difficult to find because they usually occur after a long session of app usage.
Getting Started
To get started, download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.
Throughout this tutorial you’ll work with TripLog and GuessCount.
TripLog lets the user write notes about what they’re doing and feeling during a trip. It also saves the date and location.
GuessCount is a game that asks you to internally count to a number in seconds. You press a button when you think the count is over. Then the game tells you the difference in seconds between your count and the actual count.
Open Android Studio 3.4.1 or later and click File ▸ New ▸ Import Project. Select the top-level project folder for one of the starter projects you downloaded.
Alternatively, to open one of the projects, select Open an existing Android Studio project from the Welcome screen. Again, choose the top-level project folder for one of the starter projects you downloaded.
Build and run TripLog and GuessCount to become familiar with them:
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.
- CoordinatesFormatter.kt and DateFormatter.kt format the data to show in the screens.
The GuessCount project has these files:
- MainActivity.kt contains the main screen. It lets the users set a number in seconds to count.
- CountActivity.kt lets the user press a button when he or she thinks the count is over.
Finding a Leak Using The Android Profiler
Start profiling the GuessCount starter app using the Android Profiler. You can open it by going to View ‣ Tool Windows ‣ Profiler.
Enter 240 seconds to the Seconds to count field and press the Start button. You’ll see a small loading screen. Here, you’re supposed to count to 240 and press Guess, but instead, press the Back button.
In the profiler, generate a heap dump by pressing the Dump Java heap button:
Filter by CountActivity. You’ll see this:
The CountActivity wasn’t deallocated, despite not showing in the screen. It’s possible the Garbage Collector hasn’t passed yet. So, force a few garbage collections by pressing the corresponding button in the profiler:
Now, generate a new heap dump to check if it was deallocated:
No, it wasn’t deallocated. So, click on the CountActivity row to see more detail:
In the Instance View, you’ll see that according to the mFinished
property, the activity already finished. So the Garbage collector should have collected it.
This is a memory leak because the CountActivity is still in memory but nobody needs it.
Wait a few minutes. Repeat the process of forcing a garbage collection and generating a heap dump. You’ll see that finally the activity is deallocated:
This is actually a temporal memory leak. In this example, temporarily leaking the CountActivity isn’t a big problem. But remember, leaking an entire activity could be really bad because you’d retain all its views and every object it references. This is also true for anything that has a reference to an object as well, such as a TextView
.
To analyze this problem, open the first heap dump where the CountActivity was still retained even though you closed it. Do this by selecting it:
Filter by CountActivity and select it. Then select the instance in the Instance View and you’ll see the following:
The list in the References pane shows all the objects that have a reference to the CountActivity. The first one, this$0 in CountActivity$timeoutRunnable$1
is a variable from CountActivity. So, open the CountActivity.kt file and search for this variable:
private val timeoutRunnable = object : Runnable {
override fun run() {
this@CountActivity.stopCounting()
this@CountActivity.showTimeoutResults()
}
}
This variable holds a reference to an anonymous class of the Runnable
interface. In anonymous classes you can get reference to your container, in this case the CountActivity
.
That’s why this code can call stopCounting()
and showTimeoutResults()
. If you right click over the timeoutRunnable
variable and select Find Usages, you’ll see that it’s called from this method:
private fun startCounting() {
startTimestamp = System.currentTimeMillis()
val timeoutMillis = userCount * 1000 + 10000L
timeoutHandler.postDelayed(timeoutRunnable, timeoutMillis)
}
startCounting()
is called from onCreate()
. Here, you see a timeoutHandler
that delays the execution of the timeoutRunnable
. The app uses this timeout if you don’t press the Guess button.
Continue investigating the CountActivity class and you’ll see that timeoutHandler
never cancels when you exit the activity. Therefore, it’ll execute what’s inside timeoutRunnable
after timeoutMillis
. In this case, that’s the stopCounting()
and showTimeoutResults()
methods from CountActivity. It has to retain it, generating the leak.