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.

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.

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:

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:

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. :]