main

square/leakcanary

Last updated at: 29/12/2023 09:38

LeakCanary.kt

TLDR

The file LeakCanary.kt is the entry point API for LeakCanary, a memory leak detection library for Android. It provides a configuration data class and various methods for configuring and using LeakCanary.

Methods

No methods are defined in this file.

Classes

Config

The Config class is a data class that represents the configuration options for LeakCanary. It includes various properties such as dumpHeap, retainedVisibleThreshold, and eventListeners. Each property can be updated using the copy function.

Builder

The Builder class provides a convenient way to create a new Config instance. It includes methods for setting the values of each property in the Config class. The build method returns a new Config instance with the specified values.

LeakCanary

The LeakCanary object is the main entry point for using LeakCanary. It includes a config property that represents the current configuration for LeakCanary. This property can be updated at any time by replacing it with a new Config instance.

The LeakCanary object also includes various methods for interacting with LeakCanary. This includes creating a new Intent to launch the leak display activity, showing or hiding the launcher icon for the leak display activity, and triggering a heap dump and analysis.

package leakcanary

import android.content.Intent
import leakcanary.LeakCanary.config
import leakcanary.internal.HeapDumpControl
import leakcanary.internal.InternalLeakCanary
import leakcanary.internal.InternalLeakCanary.FormFactor.TV
import leakcanary.internal.activity.LeakActivity
import shark.AndroidMetadataExtractor
import shark.AndroidObjectInspectors
import shark.AndroidReferenceMatchers
import shark.FilteringLeakingObjectFinder
import shark.HeapAnalysisSuccess
import shark.IgnoredReferenceMatcher
import shark.KeyedWeakReferenceFinder
import shark.LeakingObjectFinder
import shark.LibraryLeakReferenceMatcher
import shark.MetadataExtractor
import shark.ObjectInspector
import shark.ReferenceMatcher
import shark.SharkLog

/**
 * The entry point API for LeakCanary. LeakCanary builds on top of [AppWatcher]. AppWatcher
 * notifies LeakCanary of retained instances, which in turns dumps the heap, analyses it and
 * publishes the results.
 *
 * LeakCanary can be configured by updating [config].
 */
object LeakCanary {

  /**
   * LeakCanary configuration data class. Properties can be updated via [copy].
   *
   * @see [config]
   */
  data class Config(
    /**
     * Whether LeakCanary should dump the heap when enough retained instances are found. This needs
     * to be true for LeakCanary to work, but sometimes you may want to temporarily disable
     * LeakCanary (e.g. for a product demo).
     *
     * Defaults to true.
     */
    val dumpHeap: Boolean = true,
    /**
     * If [dumpHeapWhenDebugging] is false then LeakCanary will not dump the heap
     * when the debugger is attached. The debugger can create temporary memory leaks (for instance
     * if a thread is blocked on a breakpoint).
     *
     * Defaults to false.
     */
    val dumpHeapWhenDebugging: Boolean = false,
    /**
     * When the app is visible, LeakCanary will wait for at least
     * [retainedVisibleThreshold] retained instances before dumping the heap. Dumping the heap
     * freezes the UI and can be frustrating for developers who are trying to work. This is
     * especially frustrating as the Android Framework has a number of leaks that cannot easily
     * be fixed.
     *
     * When the app becomes invisible, LeakCanary dumps the heap after
     * [AppWatcher.retainedDelayMillis] ms.
     *
     * The app is considered visible if it has at least one activity in started state.
     *
     * A higher threshold means LeakCanary will dump the heap less often, therefore it won't be
     * bothering developers as much but it could miss some leaks.
     *
     * Defaults to 5.
     */
    val retainedVisibleThreshold: Int = 5,

    /**
     * Known patterns of references in the heap, added here either to ignore them
     * ([IgnoredReferenceMatcher]) or to mark them as library leaks ([LibraryLeakReferenceMatcher]).
     *
     * When adding your own custom [LibraryLeakReferenceMatcher] instances, you'll most
     * likely want to set [LibraryLeakReferenceMatcher.patternApplies] with a filter that checks
     * for the Android OS version and manufacturer. The build information can be obtained by calling
     * [shark.AndroidBuildMirror.fromHeapGraph].
     *
     * Defaults to [AndroidReferenceMatchers.appDefaults]
     */
    val referenceMatchers: List<ReferenceMatcher> = AndroidReferenceMatchers.appDefaults,

    /**
     * List of [ObjectInspector] that provide LeakCanary with insights about objects found in the
     * heap. You can create your own [ObjectInspector] implementations, and also add
     * a [shark.AppSingletonInspector] instance created with the list of internal singletons.
     *
     * Defaults to [AndroidObjectInspectors.appDefaults]
     */
    val objectInspectors: List<ObjectInspector> = AndroidObjectInspectors.appDefaults,

    /**
     * Extracts metadata from a hprof to be reported in [HeapAnalysisSuccess.metadata].
     * Called on a background thread during heap analysis.
     *
     * Defaults to [AndroidMetadataExtractor]
     */
    val metadataExtractor: MetadataExtractor = AndroidMetadataExtractor,

    /**
     * Whether to compute the retained heap size, which is the total number of bytes in memory that
     * would be reclaimed if the detected leaks didn't happen. This includes native memory
     * associated to Java objects (e.g. Android bitmaps).
     *
     * Computing the retained heap size can slow down the analysis because it requires navigating
     * from GC roots through the entire object graph, whereas [shark.HeapAnalyzer] would otherwise
     * stop as soon as all leaking instances are found.
     *
     * Defaults to true.
     */
    val computeRetainedHeapSize: Boolean = true,

    /**
     * How many heap dumps are kept on the Android device for this app package. When this threshold
     * is reached LeakCanary deletes the older heap dumps. As several heap dumps may be enqueued
     * you should avoid going down to 1 or 2.
     *
     * Defaults to 7.
     */
    val maxStoredHeapDumps: Int = 7,

    /**
     * LeakCanary always attempts to store heap dumps on the external storage if the
     * WRITE_EXTERNAL_STORAGE is already granted, and otherwise uses the app storage.
     * If the WRITE_EXTERNAL_STORAGE permission is not granted and
     * [requestWriteExternalStoragePermission] is true, then LeakCanary will display a notification
     * to ask for that permission.
     *
     * Defaults to false because that permission notification can be annoying.
     */
    val requestWriteExternalStoragePermission: Boolean = false,

    /**
     * Finds the objects that are leaking, for which LeakCanary will compute leak traces.
     *
     * Defaults to [KeyedWeakReferenceFinder] which finds all objects tracked by a
     * [KeyedWeakReference], ie all objects that were passed to
     * [ObjectWatcher.expectWeaklyReachable].
     *
     * You could instead replace it with a [FilteringLeakingObjectFinder], which scans all objects
     * in the heap dump and delegates the decision to a list of
     * [FilteringLeakingObjectFinder.LeakingObjectFilter]. This can lead to finding more leaks
     * than the default and shorter leak traces. This also means that every analysis during a
     * given process life will bring up the same leaking objects over and over again, unlike
     * when using [KeyedWeakReferenceFinder] (because [KeyedWeakReference] instances are cleared
     * after each heap dump).
     *
     * The list of filters can be built from [AndroidObjectInspectors]:
     *
     * ```kotlin
     * LeakCanary.config = LeakCanary.config.copy(
     *     leakingObjectFinder = FilteringLeakingObjectFinder(
     *         AndroidObjectInspectors.appLeakingObjectFilters
     *     )
     * )
     * ```
     */
    val leakingObjectFinder: LeakingObjectFinder = KeyedWeakReferenceFinder,

    /**
     * Dumps the Java heap. You may replace this with your own implementation if you wish to
     * change the core heap dumping implementation.
     */
    val heapDumper: HeapDumper = AndroidDebugHeapDumper,

    /**
     * Listeners for LeakCanary events. See [EventListener.Event] for the list of events and
     * which thread they're sent from. You most likely want to keep this list and add to it, or
     * remove a few entries but not all entries. Each listener is independent and provides
     * additional behavior which you can disable by not excluding it:
     *
     * ```kotlin
     * // No cute canary toast (very sad!)
     * LeakCanary.config = LeakCanary.config.run {
     *   copy(
     *     eventListeners = eventListeners.filter {
     *       it !is ToastEventListener
     *     }
     *   )
     * }
     * ```
     */
    val eventListeners: List<EventListener> = listOf(
      LogcatEventListener,
      ToastEventListener,
      LazyForwardingEventListener {
        if (InternalLeakCanary.formFactor == TV) TvEventListener else NotificationEventListener
      },
      when {
          RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath ->
            RemoteWorkManagerHeapAnalyzer
          WorkManagerHeapAnalyzer.validWorkManagerInClasspath -> WorkManagerHeapAnalyzer
          else -> BackgroundThreadHeapAnalyzer
      }
    ),

    /**
     * Whether to show LeakCanary notifications. When [showNotifications] is true, LeakCanary
     * will only display notifications if the app is in foreground and is not an instant, TV or
     * Wear app.
     *
     * Defaults to true.
     */
    val showNotifications: Boolean = true,
  ) {

    /**
     * Construct a new Config via [LeakCanary.Config.Builder].
     * Note: this method is intended to be used from Java code only. For idiomatic Kotlin use
     * `copy()` to modify [LeakCanary.config].
     */
    @Suppress("NEWER_VERSION_IN_SINCE_KOTLIN")
    @SinceKotlin("999.9") // Hide from Kotlin code, this method is only for Java code
    fun newBuilder() = Builder(this)

    /**
     * Builder for [LeakCanary.Config] intended to be used only from Java code.
     *
     * Usage:
     * ```java
     * LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
     *    .retainedVisibleThreshold(3)
     *    .build();
     * LeakCanary.setConfig(config);
     * ```
     *
     * For idiomatic Kotlin use `copy()` method instead:
     * ```kotlin
     * LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)
     * ```
     */
    class Builder internal constructor(config: Config) {
      private var dumpHeap = config.dumpHeap
      private var dumpHeapWhenDebugging = config.dumpHeapWhenDebugging
      private var retainedVisibleThreshold = config.retainedVisibleThreshold
      private var referenceMatchers = config.referenceMatchers
      private var objectInspectors = config.objectInspectors
      private var metadataExtractor = config.metadataExtractor
      private var computeRetainedHeapSize = config.computeRetainedHeapSize
      private var maxStoredHeapDumps = config.maxStoredHeapDumps
      private var requestWriteExternalStoragePermission =
        config.requestWriteExternalStoragePermission
      private var leakingObjectFinder = config.leakingObjectFinder
      private var heapDumper = config.heapDumper
      private var eventListeners = config.eventListeners
      private var showNotifications = config.showNotifications

      /** @see [LeakCanary.Config.dumpHeap] */
      fun dumpHeap(dumpHeap: Boolean) =
        apply { this.dumpHeap = dumpHeap }

      /** @see [LeakCanary.Config.dumpHeapWhenDebugging] */
      fun dumpHeapWhenDebugging(dumpHeapWhenDebugging: Boolean) =
        apply { this.dumpHeapWhenDebugging = dumpHeapWhenDebugging }

      /** @see [LeakCanary.Config.retainedVisibleThreshold] */
      fun retainedVisibleThreshold(retainedVisibleThreshold: Int) =
        apply { this.retainedVisibleThreshold = retainedVisibleThreshold }

      /** @see [LeakCanary.Config.referenceMatchers] */
      fun referenceMatchers(referenceMatchers: List<ReferenceMatcher>) =
        apply { this.referenceMatchers = referenceMatchers }

      /** @see [LeakCanary.Config.objectInspectors] */
      fun objectInspectors(objectInspectors: List<ObjectInspector>) =
        apply { this.objectInspectors = objectInspectors }

      /** @see [LeakCanary.Config.metadataExtractor] */
      fun metadataExtractor(metadataExtractor: MetadataExtractor) =
        apply { this.metadataExtractor = metadataExtractor }

      /** @see [LeakCanary.Config.computeRetainedHeapSize] */
      fun computeRetainedHeapSize(computeRetainedHeapSize: Boolean) =
        apply { this.computeRetainedHeapSize = computeRetainedHeapSize }

      /** @see [LeakCanary.Config.maxStoredHeapDumps] */
      fun maxStoredHeapDumps(maxStoredHeapDumps: Int) =
        apply { this.maxStoredHeapDumps = maxStoredHeapDumps }

      /** @see [LeakCanary.Config.requestWriteExternalStoragePermission] */
      fun requestWriteExternalStoragePermission(requestWriteExternalStoragePermission: Boolean) =
        apply { this.requestWriteExternalStoragePermission = requestWriteExternalStoragePermission }

      /** @see [LeakCanary.Config.leakingObjectFinder] */
      fun leakingObjectFinder(leakingObjectFinder: LeakingObjectFinder) =
        apply { this.leakingObjectFinder = leakingObjectFinder }

      /** @see [LeakCanary.Config.heapDumper] */
      fun heapDumper(heapDumper: HeapDumper) =
        apply { this.heapDumper = heapDumper }

      /** @see [LeakCanary.Config.eventListeners] */
      fun eventListeners(eventListeners: List<EventListener>) =
        apply { this.eventListeners = eventListeners }

      /** @see [LeakCanary.Config.showNotifications] */
      fun showNotifications(showNotifications: Boolean) =
        apply { this.showNotifications = showNotifications }


      fun build() = config.copy(
        dumpHeap = dumpHeap,
        dumpHeapWhenDebugging = dumpHeapWhenDebugging,
        retainedVisibleThreshold = retainedVisibleThreshold,
        referenceMatchers = referenceMatchers,
        objectInspectors = objectInspectors,
        metadataExtractor = metadataExtractor,
        computeRetainedHeapSize = computeRetainedHeapSize,
        maxStoredHeapDumps = maxStoredHeapDumps,
        requestWriteExternalStoragePermission = requestWriteExternalStoragePermission,
        leakingObjectFinder = leakingObjectFinder,
        heapDumper = heapDumper,
        eventListeners = eventListeners,
        showNotifications = showNotifications,
      )
    }
  }

  /**
   * The current LeakCanary configuration. Can be updated at any time, usually by replacing it with
   * a mutated copy, e.g.:
   *
   * ```kotlin
   * LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)
   * ```
   *
   * In Java, use [LeakCanary.Config.Builder] instead:
   * ```java
   * LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
   *    .retainedVisibleThreshold(3)
   *    .build();
   * LeakCanary.setConfig(config);
   * ```
   */
  @JvmStatic @Volatile
  var config: Config = Config()
    set(newConfig) {
      val previousConfig = field
      field = newConfig
      logConfigChange(previousConfig, newConfig)
      HeapDumpControl.updateICanHasHeapInBackground()
    }

  private fun logConfigChange(
    previousConfig: Config,
    newConfig: Config
  ) {
    SharkLog.d {
      val changedFields = mutableListOf<String>()
      Config::class.java.declaredFields.forEach { field ->
        field.isAccessible = true
        val previousValue = field[previousConfig]
        val newValue = field[newConfig]
        if (previousValue != newValue) {
          changedFields += "${field.name}=$newValue"
        }
      }
      val changesInConfig =
        if (changedFields.isNotEmpty()) changedFields.joinToString(", ") else "no changes"

      "Updated LeakCanary.config: Config($changesInConfig)"
    }
  }

  /**
   * Returns a new [Intent] that can be used to programmatically launch the leak display activity.
   */
  fun newLeakDisplayActivityIntent() = LeakActivity.createHomeIntent(InternalLeakCanary.application)

  /**
   * Dynamically shows / hides the launcher icon for the leak display activity.
   * Note: you can change the default value by overriding the `leak_canary_add_launcher_icon`
   * boolean resource:
   *
   * ```xml
   * <?xml version="1.0" encoding="utf-8"?>
   * <resources>
   *   <bool name="leak_canary_add_launcher_icon">false</bool>
   * </resources>
   * ```
   */
  fun showLeakDisplayActivityLauncherIcon(showLauncherIcon: Boolean) {
    InternalLeakCanary.setEnabledBlocking(
      "leakcanary.internal.activity.LeakLauncherActivity", showLauncherIcon
    )
  }

  /**
   * Immediately triggers a heap dump and analysis, if there is at least one retained instance
   * tracked by [AppWatcher.objectWatcher]. If there are no retained instances then the heap will not
   * be dumped and a notification will be shown instead.
   */
  fun dumpHeap() = InternalLeakCanary.onDumpHeapReceived(forceDump = true)
}