Skip to content
🤔 Documentation issue? Report or edit

Code Recipes

This page contains code recipes to customize LeakCanary to your needs. Read through the section titles and cook your own meal! Also don’t forget to check out the FAQ.

Bug

If you think a recipe might be missing or you’re not sure that what you’re trying to achieve is possible with the current APIs, please file an issue. Your feedback helps us make LeakCanary better for the entire community.

Watching objects with a lifecycle

The default configuration of LeakCanary will automatically watch Activity, Fragment, Fragment View and ViewModel instances.

In your application, you may have other objects with a lifecycle, such as services, Dagger components, etc. Use AppWatcher.objectWatcher to watch instances that should be garbage collected:

class MyService : Service {

  // ...

  override fun onDestroy() {
    super.onDestroy()
    AppWatcher.objectWatcher.watch(
      watchedObject = this,
      description = "MyService received Service#onDestroy() callback"
    )
  }
}

Watching dismissed dialogs

LeakCanary watches root views that stay in memory after View.onDetachedFromWindow(), but it skips the root views of dialogs by default. A single Dialog instance can be shown and dismissed several times, so its view hierarchy can be attached and detached several times, and watching every dismissed dialog would report leaks that aren’t leaks.

If your codebase has a rule that a dialog is created where it’s needed and shown once, that ambiguity doesn’t exist and dismissed dialogs are worth watching. Turn it on with a resource boolean:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <bool name="leak_canary_watcher_watch_dismissed_dialogs">true</bool>
</resources>

Configuration

LeakCanary has a default configuration that works well for most apps. You can also customize it to your needs. The LeakCanary configuration is held by two singleton objects (AppWatcher and LeakCanary) and can be updated at any time. Most developers configure LeakCanary in their debug Application class:

class DebugExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()
    AppWatcher.config = AppWatcher.config.copy(watchFragmentViews = false)
  }
}

Info

Create a debug application class in your src/debug/java folder. Don’t forget to also register it in src/debug/AndroidManifest.xml.

To customize the detection of retained objects at runtime, specify the watchers you wish to install via AppWatcher.manualInstall():

val watchersToInstall = AppWatcher.appDefaultWatchers(this)
  .filter { it !is FragmentAndViewModelWatcher }
AppWatcher.manualInstall(
  application = this,
  watchersToInstall = watchersToInstall
)

To customize the heap dumping & analysis, update LeakCanary.config:

LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)

Java

In Java, use LeakCanary.Config.Builder instead:

LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
   .retainedVisibleThreshold(3)
   .build();
LeakCanary.setConfig(config);

Configure the LeakCanary UI by overriding the following resources:

Disabling LeakCanary

Sometimes it’s necessary to disable LeakCanary temporarily, for example for a product demo or when running performance tests. You have different options, depending on what you’re trying to achieve:

  • Create a build variant that does not include the LeakCanary dependencies, see Setting up LeakCanary for different product flavors.
  • Disable the heap dumping & analysis: LeakCanary.config = LeakCanary.config.copy(dumpHeap = false).
  • Hide the leak display activity launcher icon: override R.bool.leak_canary_add_launcher_icon or call LeakCanary.showLeakDisplayActivityLauncherIcon(false)

Info

When you set LeakCanary.Config.dumpHeap to false, AppWatcher.objectWatcher will still keep track of retained objects, and LeakCanary will look for these objects when you change LeakCanary.Config.dumpHeap back to true.

LeakCanary test environment detection

By default, LeakCanary looks for the fully qualified runtime class name org.junit.Test. If that class is found, LeakCanary disables automatic heap dumping to avoid running during tests. Some apps include JUnit on their debug runtime classpath outside tests (for example, when using OkHttp’s MockWebServer), so you can override the class name used for this check.

In your app module, create a values resource file under src/debug/res/values/ (or the corresponding source set for the build variant that includes LeakCanary). For example, create src/debug/res/values/leak_canary.xml:

<resources>
  <string name="leak_canary_test_class_name">assertk.Assert</string>
</resources>

The XML filename does not matter; only the leak_canary_test_class_name resource name does. assertk.Assert is an example fully qualified runtime class name, not a special LeakCanary value. Replace it with a class that is present at runtime in the test environments you want detected and absent otherwise.

This resource supports one class name, not multiple rules. LeakCanary reads exactly one configured resource value and passes it directly to Class.forName(). Do not provide a list or comma-separated class names. If several test setups should disable automatic heap dumping, configure one class that is common to all of them. If no single class fits, use this resource for one class and handle the remaining conditions in app code by setting LeakCanary.config.dumpHeap to false when they apply; setting it to true does not override class detection.

Counting retained instances in release builds

The com.squareup.leakcanary:leakcanary-android dependency should only be used in debug builds. It depends on com.squareup.leakcanary:leakcanary-object-watcher-android which you can use in release builds to track and count retained instances.

In your build.gradle:

dependencies {
  implementation 'com.squareup.leakcanary:leakcanary-object-watcher-android:2.14'
}

In your leak reporting code:

val retainedInstanceCount = AppWatcher.objectWatcher.retainedObjectCount

LeakCanary in release builds

We do not recommend including LeakCanary in release builds, as it could negatively impact the experience of your customers. To avoid accidentally including the com.squareup.leakcanary:leakcanary-android dependency in a release build, LeakCanary crashes during initialization if the APK is not debuggable. You may have a good reason to create a non debuggable build that includes LeakCanary, for example for a QA build. If necessary, the crashing check can be disabled by overriding the bool/leak_canary_allow_in_non_debuggable_build resource, e.g. by creating a file under res/values with the following contents:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <bool name="leak_canary_allow_in_non_debuggable_build">true</bool>
</resources>

Android TV

LeakCanary works on Android TV devices (FireTV, Nexus player, Nvidia Shield, MiBox, etc.) without any additional setup. However, there are couple things you need to be aware of:

  • Android TV doesn’t have notifications. LeakCanary will display Toast messages when objects become retained and when leak analysis completes. You can also check Logcat for more details.
  • Due to lack of notifications, the only way to manually trigger a heap dump is to background the app.
  • There’s a bug on API 26+ devices that prevents the activity that displays leaks from appearing in apps list. As a workaround, LeakCanary prints an adb shell command in Logcat after heap dump analysis that launches leak list activity:
    adb shell am start -n "com.your.package.name/leakcanary.internal.activity.LeakLauncherActivity"
    
  • Some Android TV devices have very little memory available per app process and this might impact LeakCanary. Running the LeakCanary analysis in a separate process might help in such cases.

Icon and label

The activity that displays leaks comes with a default icon and label, which you can change by providing R.mipmap.leak_canary_icon and R.string.leak_canary_display_activity_label in your app:

res/
  mipmap-hdpi/
    leak_canary_icon.png
  mipmap-mdpi/
    leak_canary_icon.png
  mipmap-xhdpi/
    leak_canary_icon.png
  mipmap-xxhdpi/
    leak_canary_icon.png
  mipmap-xxxhdpi/
    leak_canary_icon.png
   mipmap-anydpi-v26/
     leak_canary_icon.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="leak_canary_display_activity_label">MyLeaks</string>
</resources>

Matching known library leaks

Set LeakCanary.Config.referenceMatchers to a list that builds on top of AndroidReferenceMatchers.appDefaults:

class DebugExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()
    LeakCanary.config = LeakCanary.config.copy(
        referenceMatchers = AndroidReferenceMatchers.appDefaults +
            AndroidReferenceMatchers.staticFieldLeak(
                className = "com.samsing.SomeSingleton",
                fieldName = "sContext",
                description = "SomeSingleton has a static field leaking a context.",
                patternApplies = {
                  manufacturer == "Samsing" && sdkInt == 26
                }
            )
    )
  }
}

Ignoring specific activities or fragment classes

Sometimes a 3rd party library provides its own activities or fragments which contain a number of bugs leading to leaks of those specific 3rd party activities and fragments. You should push hard on that library to fix their memory leaks as it’s directly impacting your application. That being said, until those are fixed, you have two options:

  1. Add the specific leaks as known library leaks (see Matching known library leaks). LeakCanary will run when those leaks are detected and then report them as known library leaks.
  2. Disable LeakCanary automatic activity or fragment watching (e.g. AppWatcher.config = AppWatcher.config.copy(watchActivities = false)) and then manually pass objects to AppWatcher.objectWatcher.watch.

Identifying leaking objects and labeling objects

Working out whether an object in a leak trace should still be in memory is something you do by reading your own code, and the answer is a property of the type, not of that one heap dump — so it will hold the next time too. An ObjectInspector is how you write that conclusion down once and have LeakCanary reach it on its own from then on, narrowing the suspect references automatically.

An inspector is called for every object in a leak trace. Add leakingReasons or notLeakingReasons to state a conclusion, and labels to attach information that helps without settling anything:

class DebugExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()
    val addEntityIdLabel = ObjectInspector { reporter ->
      reporter.whenInstanceOf("com.example.DbEntity") { instance ->
        val databaseIdField = instance["com.example.DbEntity", "databaseId"]!!
        val databaseId = databaseIdField.value.asInt!!
        labels += "DbEntity.databaseId = $databaseId"
      }
    }

    val singletonsInspector =
      AppSingletonInspector("com.example.MySingleton", "com.example.OtherSingleton")

    val mmvmInspector = ObjectInspector { reporter ->
      reporter.whenInstanceOf("com.mmvm.SomeViewModel") { instance ->
        val destroyedField = instance["com.mmvm.SomeViewModel", "destroyed"]!!
        if (destroyedField.value.asBoolean!!) {
          leakingReasons += "SomeViewModel.destroyed is true"
        } else {
          notLeakingReasons += "SomeViewModel.destroyed is false"
        }
      }
    }

    LeakCanary.config = LeakCanary.config.copy(
        objectInspectors = AndroidObjectInspectors.appDefaults +
            listOf(addObjectIdLabel, singletonsInspector, mmvmInspector)
    )
  }
}

Deciding from objects further down the graph

An inspector isn’t limited to the fields of the object it was called for. It receives a HeapInstance, which can be navigated like any other object in the heap dump, so a conclusion about one object can be drawn from the state of another.

That is often the only way to state a lifecycle. An object whose lifetime is tied to something it holds — a helper that lives exactly as long as the view it was created for — is leaking exactly when that view is, and the way to say so is to go and look at the view:

val jankStatsInspector = ObjectInspector { reporter ->
  reporter.whenInstanceOf("androidx.metrics.performance.JankStats") { jankStats ->
    // A JankStats lives as long as the window it tracks, so it's leaking if
    // the decor view of that window has been detached.
    val decorView = jankStats["androidx.metrics.performance.JankStats", "implementation"]
      ?.valueAsInstance
      ?.get("androidx.metrics.performance.JankStatsApi24Impl", "window")
      ?.valueAsInstance
      ?.get("com.android.internal.policy.PhoneWindow", "mDecor")
      ?.valueAsInstance

    val attachInfo = decorView?.get("android.view.View", "mAttachInfo")
    if (attachInfo != null) {
      if (attachInfo.value.isNullReference) {
        leakingReasons += "JankStats is retaining a detached DecorView"
      } else {
        notLeakingReasons += "JankStats is retaining an attached DecorView"
      }
    }
  }
}

Every step returns null when the field or the class isn’t what you expected, so an inspector written this way says nothing rather than saying something wrong on a device where the internals differ. That matters: a wrong notLeakingReasons hides the object that was actually holding the leak.

The LeakCanary Method walks through the investigation these two inspectors came out of.

Running the LeakCanary analysis in a separate process

LeakCanary runs in your main app process. LeakCanary 2 is optimized to keep memory usage low while analysing and runs in a background thread with priority Process.THREAD_PRIORITY_BACKGROUND. If you find that LeakCanary is still using too much memory or impacting the app process performance, you can configure it to run the analysis in a separate process.

All you have to do is replace the leakcanary-android dependency with leakcanary-android-process:

dependencies {
  // debugImplementation 'com.squareup.leakcanary:leakcanary-android:${version}'
  debugImplementation 'com.squareup.leakcanary:leakcanary-android-process:${version}'
}

You can call LeakCanaryProcess.isInAnalyzerProcess to check if your Application class is being created in the LeakCanary process. This is useful when configuring libraries like Firebase that may crash when running in an unexpected process.

Event listeners and the analyzer process

Your Application class is created in both processes, so LeakCanary.Config.eventListeners is normally configured in both as well, and it matters which process each event is dispatched from:

  • HeapAnalysisDone is dispatched from the main process. LeakCanary stores the analysis in its database from the :leakcanary process, then runs a WorkManager worker in the main process which reads the analysis back and dispatches the event. This also means that if the main process dies while the analysis is running, the event is dispatched the next time the main process starts, rather than being lost.
  • HeapAnalysisProgress is dispatched from the :leakcanary process, since that’s where the analysis runs.
  • All other events (DumpingHeap, HeapDump, HeapDumpFailed) are dispatched from the main process, since that’s where the heap dump is triggered.

So a custom listener that only cares about the analysis result can be configured in the main process:

class DebugExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()
    if (LeakCanaryProcess.isInAnalyzerProcess(this)) {
      return
    }
    LeakCanary.config = LeakCanary.config.run {
      copy(eventListeners = eventListeners + EventListener { event ->
        if (event is HeapAnalysisDone<*>) {
          uploadToMyBackend(event.heapAnalysis)
        }
      })
    }
  }
}

Setting up LeakCanary for different product flavors

You can setup LeakCanary to run in a specific product flavors of your app. For example, create:

android {
  flavorDimensions "default"
  productFlavors {
    prod {
      // ...
    }
    qa {
      // ...
    }
    dev {
      // ...
    }
  }
}

Then, define a custom configuration for the flavor for which you want to enable LeakCanary:

android {
  // ...
}
configurations {
    devDebugImplementation {}
}

You can now add the LeakCanary dependency for that configuration:

dependencies {
  devDebugImplementation "com.squareup.leakcanary:leakcanary-android:${version}"
}

Extracting metadata from the heap dump

LeakCanary.Config.metadataExtractor extracts metadata from a heap dump. The metadata is then available in HeapAnalysisSuccess.metadata. LeakCanary.Config.metadataExtractor defaults to AndroidMetadataExtractor but you can replace it to extract additional metadata from the hprof.

For example, if you want to include the app version name in your heap analysis reports, you need to first store it in memory (e.g. in a static field) and then you can retrieve it in MetadataExtractor.

class DebugExampleApplication : ExampleApplication() {

  companion object {
    @JvmStatic
    lateinit var savedVersionName: String
  }

  override fun onCreate() {
    super.onCreate()

    val packageInfo = packageManager.getPackageInfo(packageName, 0)
    savedVersionName = packageInfo.versionName

    LeakCanary.config = LeakCanary.config.copy(
        metadataExtractor = MetadataExtractor { graph ->
          val companionClass =
            graph.findClassByName("com.example.DebugExampleApplication")!!

          val versionNameField = companionClass["savedVersionName"]!!
          val versionName = versionNameField.valueAsInstance!!.readAsJavaString()!!

          val defaultMetadata = AndroidMetadataExtractor.extractMetadata(graph)

          mapOf("App Version Name" to versionName) + defaultMetadata
        })
  }
}

Using LeakCanary with obfuscated apps

If obfuscation is turned on then leak traces will be obfuscated. It’s possible to automatically deobfuscate leak traces by using a deobfuscation gradle plugin provided by LeakCanary.

You have to add a plugin dependency in your root build.gradle file:

buildscript {
  dependencies {
    classpath 'com.squareup.leakcanary:leakcanary-deobfuscation-gradle-plugin:${version}'
  }
}

And then you need to apply and configure the plugin in your app (or library) specific build.gradle file:

apply plugin: 'com.android.application'
apply plugin: 'com.squareup.leakcanary.deobfuscation'

leakCanary {
  // LeakCanary needs to know which variants have obfuscation turned on
  filterObfuscatedVariants { variant ->
    variant.name == "debug"
  }
}

Now you can run LeakCanary on an obfuscated app and leak traces will be automatically deobfuscated.

The variant passed to filterObfuscatedVariants is a Variant from the Android Gradle Plugin Variant API, so the plugin requires the Android Gradle Plugin 8.0 or newer. The task that copies the mapping file is named copy${VariantName}LeakCanaryObfuscationMapping.

Important: never use this plugin on a release variant. This plugin copies obfuscation mapping file and puts it inside the .apk, so if you use it on release build then the obfuscation becomes pointless because the code can be easily deobfuscated using mapping file.

Warning: R8 (Google Proguard replacement) can now understand Kotlin language constructs but the side effect is that mapping files can get very large (a couple dozen megabytes). It means that the size of .apk containing copied mapping file will increase as well. This is another reason for not using this plugin on a release variant.

Detecting leaks in JVM applications

While LeakCanary was designed to work out of the box on Android, it can run on any JVM with a bit of configuration.

Add the ObjectWatcher and Shark dependencies to your build file:

dependencies {
  implementation 'com.squareup.leakcanary:leakcanary-object-watcher:2.14'
  implementation 'com.squareup.leakcanary:shark:2.14'
}

Define a HotSpotHeapDumper to dump the heap:

import com.sun.management.HotSpotDiagnosticMXBean
import java.lang.management.ManagementFactory

object HotSpotHeapDumper {
  private val mBean: HotSpotDiagnosticMXBean by lazy {
    val server = ManagementFactory.getPlatformMBeanServer()
    ManagementFactory.newPlatformMXBeanProxy(
        server,
        "com.sun.management:type=HotSpotDiagnostic",
        HotSpotDiagnosticMXBean::class.java
    )
  }

  fun dumpHeap(fileName: String) {
    mBean.dumpHeap(fileName, LIVE)
  }

  private const val LIVE = true
}

Define a JvmHeapAnalyzer to analyze the heap when objects are retained and print the result to the console:

import leakcanary.GcTrigger
import leakcanary.ObjectWatcher
import leakcanary.OnObjectRetainedListener
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale.US

class JvmHeapAnalyzer(private val objectWatcher: ObjectWatcher) :
    OnObjectRetainedListener {

  private val fileNameFormat = SimpleDateFormat(DATE_PATTERN, US)

  override fun onObjectRetained() {
    GcTrigger.Default.runGc()
    if (objectWatcher.retainedObjectCount == 0) {
      return
    }
    val fileName = fileNameFormat.format(Date())
    val hprofFile = File(fileName)

    println("Dumping the heap to ${hprofFile.absolutePath}")
    HotSpotHeapDumper.dumpHeap(hprofFile.absolutePath)

    val analyzer = HeapAnalyzer(
        OnAnalysisProgressListener { step ->
          println("Analysis in progress, working on: ${step.name}")
        })

    val heapDumpAnalysis = analyzer.analyze(
        heapDumpFile = hprofFile,
        leakingObjectFinder = KeyedWeakReferenceFinder,
        computeRetainedHeapSize = true,
        objectInspectors = ObjectInspectors.jdkDefaults
    )
    println(heapDumpAnalysis)
  }
  companion object {
    private const val DATE_PATTERN = "yyyy-MM-dd_HH-mm-ss_SSS'.hprof'"
  }
}

Create an ObjectWatcher instance and configure it to watch objects for 5 seconds before notifying a JvmHeapAnalyzer instance:

val scheduledExecutor = Executors.newSingleThreadScheduledExecutor()
val objectWatcher = ObjectWatcher(
    clock = Clock {
      System.currentTimeMillis()
    },
    checkRetainedExecutor = Executor { command ->
      scheduledExecutor.schedule(command, 5, SECONDS)
    }
)

val heapAnalyzer = JvmHeapAnalyzer(objectWatcher)
objectWatcher.addOnObjectRetainedListener(heapAnalyzer)

Pass objects that you expect to be garbage collected (e.g. closed resources) to the ObjectWatcher instance:

objectWatcher.watch(
    watchedObject = closedResource,
    description = "$closedResource is closed and should be garbage collected"
)

If you end up using LeakCanary on a JVM, the community will definitely benefit from your experience, so don’t hesitate to let us know!

PackageManager.getLaunchIntentForPackage() returns LeakLauncherActivity

LeakCanary adds a main activity that has a Intent#CATEGORY_LAUNCHER category. PackageManager.getLaunchIntentForPackage() looks for a main activity in the category Intent#CATEGORY_INFO, and next for a main activity in the category Intent#CATEGORY_LAUNCHER. PackageManager.getLaunchIntentForPackage() returns the first activity that matches in the merged manifest of your app. If your app relies on PackageManager.getLaunchIntentForPackage(), you have two options:

  • Add Intent#CATEGORY_INFO to your main activity intent filter, so that it gets picked up first. This is what the Android documentation recommends.
  • Disable the leakcanary launcher activity by setting the leak_canary_add_launcher_icon resource boolean to false.