Android & Kotlin Tutorials

Learn Android development in Kotlin, from beginner to advanced.

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.

4.6/5 9 Ratings

Version

  • Kotlin 1.3, Android 4.4, Android Studio 3.4

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.

Note: This tutorial assumes you have previous experience with 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 Getting Started and other Android tutorials.

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.

Note: If you want to learn more about the Garbage Collector and Android Memory Management, check out this tutorial.

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:

trip log app main screen trip log app detail screen
guess count app main screen guess count app guess 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.
  • 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.

android profiler button

Note: You could also build and directly profile the app by going to Run ‣ Profile ‘app’ or pressing the Profile button in the Navigation Bar.

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:

android memory profiler dump heap

Filter by CountActivity. You’ll see this:

android memory profiler count activity not deallocated

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:

android memory profiler force gc

Now, generate a new heap dump to check if it was deallocated:

android memory profiler count activity not deallocated after gc

No, it wasn’t deallocated. So, click on the CountActivity row to see more detail:

android memory profiler count activity not deallocated after gc 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:

android memory profiler count activity 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.

Note: The Garbage Collector can’t deallocate objects the leaked activity references because it thinks this activity still needs them.

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:

android memory profiler select first heap dump

Filter by CountActivity and select it. Then select the instance in the Instance View and you’ll see the following:

android memory profiler timeoutRunnable

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.

Common Memory Leaks

You’ve experienced one case of memory leaks. Now, you’ll learn some other common cases and what cause them. You’ll also learn how to detect and avoid them using the Android Profiler and Leak Canary.

Anonymous Classes

Sometimes an anonymous class instance lives longer than the container instance is supposed to. If this anonymous class instance calls any method, or reads or writes any property of the container class, it would retain the container instance. This would leak the memory of the container.

Note: If the anonymous class instance doesn’t call any method, nor read or write any property of the container class, then it wouldn’t retain it. In that case, you wouldn’t be leaking the memory of the container.

How Leak Canary Shows a Memory Leak

Before fixing the leak, you’ll use LeakCanary to see how this tool shows when there’s a leak. So, open the build.gradle file and add the following to your dependencies:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-3'

Build and run the app, click the Start button and immediately press back. You’ll see the following:

leak canary icon leak canary notification tap

LeakCanary monitors a destroyed activity. It waits for five seconds and then forces a garbage collection. If the activity is still around, LeakCanary considers it retained and potentially leaking.

Click the notification and it’ll start dumping the heap:

leak canary dumping heap leak canary analysis done

After dumping the heap, LeakCanary will find the shortest strong reference path from GC roots to the retained instance. This is also called the leak trace.

Click the notification. Wait a few seconds while it analyzes the heap dump. Click the notification again and navigate through the LeakCanary app to see the leak trace:

leak canary countactivity leak

Note: The leak trace is also logged to Logcat.

At the bottom, it says that CountActivity is leaking because mDestroyed is true. This is the same analysis you did before with the Android Profiler.

At the top, you’ll see an instance of MessageQueue isn’t leaking. The justification is that it’s a GC root.

LeakCanary uses heuristics to determine the lifecycle state of the chain nodes and say whether or not they are leaking. A reference that is after the last Leaking: NO and before the first Leaking: YES caused the leak. Those red underlined references are the candidates.

If you go from top to bottom you’ll find CountActivity$timeoutRunnable$1.this$0. Again, this is the same variable you found when using the Android Profiler. This confirms that the timeoutRunnable is referencing the activity and preventing the Garbage Collector from deallocating it.

To fix this leak, open CountActivity.kt and add the following:

  override fun onDestroy() {
    stopCounting()
    super.onDestroy()
  }

The stopCounting() method calls the timeoutHandler.removeCallbacksAndMessages() method.

Build and run the app again to see if the leak was fixed by either using the Android Profiler or Leak Canary.

Inner Classes

Inner classes are another common source of memory leaks because they can also have a reference to their container classes. For example, suppose you have the following AsyncTask:

class MyActivity : AppCompatActivity() {
  ...
  inner class MyTask : AsyncTask<Void, Void, String>() {
    override fun doInBackground(vararg params: Void): String {
      // Perform heavy operation and return a result
    }
    override fun onPostExecute(result: String) {
      this@MyActivity.showResult(result)
    }
  }
}

This will generate a leak if you leave the activity and the task didn’t finish. You could try to cancel the AsyncTask in the onDestroy() method. However, because of how AsyncTask works, the doInBackground() method won’t cancel and you’d still continue leaking the activity.

To fix it, you should remove the inner modifier to convert MyTask to a static class. Static inner classes don’t have access to the container class, so you won’t be able to leak the activity. But you wouldn’t be able to call showResult either.

So, you may think of passing the activity as a parameter, like this:

class MyActivity : AppCompatActivity() {
  ...
  class MyTask(private val activity: MainActivity) : AsyncTask<Void, Void, String>() {
    override fun doInBackground(vararg params: Void): String {
      // Perform heavy operation and return a result
    }
    override fun onPostExecute(result: String) {
      activity.showResult(result)
    }
  }
}

However, you’d be leaking the activity as well. A possible solution would be to use a WeakReference:

class MyActivity : AppCompatActivity() {
  ...
  class MyTask(activity: MainActivity) : AsyncTask<Void, Void, String>() {
    private val weakRef = WeakReference<MyActivity>(activity)
    override fun doInBackground(vararg params: Void): String {
      // Perform heavy operation and return a result
    }
    override fun onPostExecute(result: String) {
      weakRef.get()?.showResult(result)
    }
  }
}

A WeakReference references an object like a normal reference, but isn’t strong enough to retain it in memory. Therefore, when the Garbage Collector passes and doesn’t find any strong references to the object, it’ll collect it. Any WeakReference referencing the collected object will be set to null.

Note: You should pay special attention to your code to avoid these types of leaks when combining Anonymous or Inner Classes. You also need to pay extra attention when working with delayed tasks or anything related to threads, such as Handlers, TimerTasks, Threads, AsyncTasks or RxJava.

Static Variables

The variables inside a companion object are static variables. These variables are associated with a class and not with instances of the class. So, they’ll live since the system loads the class.

You should avoid having static variables referencing activities. They won’t be garbage collected even though they’re no longer needed.

Singletons

If the singleton object holds a reference to an activity and lives longer than your activity, you’ll be leaking it.

As a workaround, you could provide a method in the singleton object that clears the reference. You could call that method in the activity’s onDestroy().

Registering Listeners

On many Android APIs and external SDKs, you have to subscribe an activity as a listener and provide callback methods to receive events. For example, you’d need to do this for location updates and system events.

This can generate a memory leak if you forget to unsubscribe. Usually, the object you subscribe to lives longer than your activity.

To see this type of memory leak in action, open the TripLog starter project. Build and run it.

Press the + button to add a new log. Accept the location permission and you’ll see it shows the location of the user. You may have to turn on Location services for your device or try adding the log a second time to see the location of the user:

trip log with location

Open the Android Profiler and start profiling the app. Press Back, force garbage collection a few times and generate a heap dump.

You’ll notice the DetailActivity is still around.

If you want, add LeakCanary to the dependencies and check the leak.

To fix this, you need to add the following:

  override fun onPause() {
    fusedLocationClient.removeLocationUpdates(locationCallback)
    super.onPause()
  }

Profile the app again or use LeakCanary. Add a new log.

Exit the app and force garbage collection. You’ll notice that it’s still leaking, but the leak is different than before.

This time the problem is the play-services-location library you’re using.

To fix it, create a file WeakLocationCallback.kt with the following content:

class WeakLocationCallback(locationCallback: LocationCallback) 
    : LocationCallback() {

    private val locationCallbackRef = WeakReference(locationCallback)

    override fun onLocationResult(locationResult: LocationResult?) {
        locationCallbackRef.get()?.onLocationResult(locationResult)
    }

    override fun onLocationAvailability(
        locationAvailability: LocationAvailability?
    ) {
        locationCallbackRef.get()?.onLocationAvailability(locationAvailability)
    }
}

Open DetailActivity.kt and replace the line where you set the locationCallback with this:

locationCallback = WeakLocationCallback(object : LocationCallback() {
  ...
})

Build and run again to check if there are any leaks. :]

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 and LeakCanary to detect memory leaks.

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

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

Average Rating

4.6/5

Add a rating for this content

9 ratings

Contributors

Comments