xref: /aosp_15_r20/external/leakcanary2/docs/recipes.md (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)
1# Code Recipes
2
3This 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](faq.md).
4
5!!! bug
6    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](https://github.com/square/leakcanary/issues/new/choose). Your feedback helps us make LeakCanary better for the entire community.
7
8## Watching objects with a lifecycle
9
10The default configuration of LeakCanary will automatically watch Activity, Fragment, Fragment View and ViewModel instances.
11
12In your application, you may have other objects with a lifecycle, such as services, Dagger components, etc. Use [AppWatcher.objectWatcher](/leakcanary/api/leakcanary/-app-watcher/object-watcher/) to watch instances that should be garbage collected:
13
14```kotlin
15class MyService : Service {
16
17  // ...
18
19  override fun onDestroy() {
20    super.onDestroy()
21    AppWatcher.objectWatcher.watch(
22      watchedObject = this,
23      description = "MyService received Service#onDestroy() callback"
24    )
25  }
26}
27```
28
29## Configuration
30
31LeakCanary 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](https://developer.android.com/reference/android/app/Application) class:
32
33```kotlin
34class DebugExampleApplication : ExampleApplication() {
35
36  override fun onCreate() {
37    super.onCreate()
38    AppWatcher.config = AppWatcher.config.copy(watchFragmentViews = false)
39  }
40}
41```
42
43!!! info
44    Create a debug application class in your `src/debug/java` folder. Don't forget to also register it in `src/debug/AndroidManifest.xml`.
45
46To customize the detection of retained objects at runtime, specify the watchers you wish to install via [AppWatcher.manualInstall()](/leakcanary/api/leakcanary/-app-watcher/manual-install/):
47
48```kotlin
49val watchersToInstall = AppWatcher.appDefaultWatchers(this)
50  .filter { it !is FragmentAndViewModelWatcher }
51AppWatcher.manualInstall(
52  application = this,
53  watchersToInstall = watchersToInstall
54)
55```
56
57To customize the heap dumping & analysis, update [LeakCanary.config](/leakcanary/api/leakcanary/-leak-canary/config/):
58
59```kotlin
60LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)
61```
62
63!!! info "Java"
64    In Java, use [LeakCanary.Config.Builder](/leakcanary/api/leakcanary/-leak-canary/-config/-builder/) instead:
65
66    ```java
67    LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
68       .retainedVisibleThreshold(3)
69       .build();
70    LeakCanary.setConfig(config);
71    ```
72
73Configure the LeakCanary UI by overriding the following resources:
74
75* `mipmap/leak_canary_icon` see [Icon and label](#icon-and-label)
76* `string/leak_canary_display_activity_label` see [Icon and label](#icon-and-label)
77* `bool/leak_canary_add_dynamic_shortcut` see [Disabling LeakCanary](#disabling-leakcanary)
78* `bool/leak_canary_add_launcher_icon` see [Disabling LeakCanary](#disabling-leakcanary)
79* `layout/leak_canary_heap_dump_toast` the layout for the toast shown when the heap is dumped
80
81## Disabling LeakCanary
82
83Sometimes 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:
84
85* Create a build variant that does not include the LeakCanary dependencies, see [Setting up LeakCanary for different product flavors](#setting-up-leakcanary-for-different-product-flavors).
86* Disable the heap dumping & analysis: `LeakCanary.config = LeakCanary.config.copy(dumpHeap = false)`.
87* Hide the leak display activity launcher icon: override `R.bool.leak_canary_add_launcher_icon` or call `LeakCanary.showLeakDisplayActivityLauncherIcon(false)`
88
89!!! info
90    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`.
91
92## LeakCanary test environment detection
93
94By default, LeakCanary will look for the `org.junit.Test` class in your classpath and if found, will disable itself to avoid running in tests. However, some apps may ship JUnit in their debug classpaths (for example, when using OkHttp's MockWebServer) so we offer a way to customise the class that is used to determine that the app is running in a test environment.
95
96```xml
97<resources>
98  <string name="leak_canary_test_class_name">assertk.Assert</string>
99</resources>
100```
101
102## Counting retained instances in release builds
103
104The `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.
105
106In your `build.gradle`:
107
108```gradle
109dependencies {
110  implementation 'com.squareup.leakcanary:leakcanary-object-watcher-android:{{ leak_canary.release }}'
111}
112```
113
114In your leak reporting code:
115```kotlin
116val retainedInstanceCount = AppWatcher.objectWatcher.retainedObjectCount
117```
118
119## LeakCanary in release builds
120
121We **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:
122
123```xml
124<?xml version="1.0" encoding="utf-8"?>
125<resources>
126  <bool name="leak_canary_allow_in_non_debuggable_build">true</bool>
127</resources>
128```
129
130## Android TV
131
132LeakCanary 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:
133
134-   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.
135-   Due to lack of notifications, the only way to **manually** trigger a heap dump is to background the app.
136-   There's a [bug on API 26+ devices](https://issuetracker.google.com/issues/141429184) 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:
137    ```
138    adb shell am start -n "com.your.package.name/leakcanary.internal.activity.LeakLauncherActivity"
139    ```
140-   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](#running-the-leakcanary-analysis-in-a-separate-process) might help in such cases.
141
142## Icon and label
143
144The 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:
145
146```
147res/
148  mipmap-hdpi/
149    leak_canary_icon.png
150  mipmap-mdpi/
151    leak_canary_icon.png
152  mipmap-xhdpi/
153    leak_canary_icon.png
154  mipmap-xxhdpi/
155    leak_canary_icon.png
156  mipmap-xxxhdpi/
157    leak_canary_icon.png
158   mipmap-anydpi-v26/
159     leak_canary_icon.xml
160```
161
162```xml
163<?xml version="1.0" encoding="utf-8"?>
164<resources>
165  <string name="leak_canary_display_activity_label">MyLeaks</string>
166</resources>
167```
168
169## Matching known library leaks
170
171Set [LeakCanary.Config.referenceMatchers](/leakcanary/api/leakcanary/-leak-canary/-config/reference-matchers/) to a list that builds on top of [AndroidReferenceMatchers.appDefaults](/leakcanary/api/shark/-android-reference-matchers/-companion/app-defaults/):
172
173```kotlin
174class DebugExampleApplication : ExampleApplication() {
175
176  override fun onCreate() {
177    super.onCreate()
178    LeakCanary.config = LeakCanary.config.copy(
179        referenceMatchers = AndroidReferenceMatchers.appDefaults +
180            AndroidReferenceMatchers.staticFieldLeak(
181                className = "com.samsing.SomeSingleton",
182                fieldName = "sContext",
183                description = "SomeSingleton has a static field leaking a context.",
184                patternApplies = {
185                  manufacturer == "Samsing" && sdkInt == 26
186                }
187            )
188    )
189  }
190}
191```
192
193## Ignoring specific activities or fragment classes
194
195Sometimes 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:
196
1971. Add the specific leaks as known library leaks (see [Matching known library leaks](#matching-known-library-leaks)). LeakCanary will run when those leaks are detected and then report them as known library leaks.
1982. 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`.
199
200## Identifying leaking objects and labeling objects
201
202```kotlin
203class DebugExampleApplication : ExampleApplication() {
204
205  override fun onCreate() {
206    super.onCreate()
207    val addEntityIdLabel = ObjectInspector { reporter ->
208      reporter.whenInstanceOf("com.example.DbEntity") { instance ->
209		val databaseIdField = instance["com.example.DbEntity", "databaseId"]!!
210		val databaseId = databaseIdField.value.asInt!!
211        labels += "DbEntity.databaseId = $databaseId"
212      }
213    }
214
215    val singletonsInspector =
216      AppSingletonInspector("com.example.MySingleton", "com.example.OtherSingleton")
217
218    val mmvmInspector = ObjectInspector { reporter ->
219      reporter.whenInstanceOf("com.mmvm.SomeViewModel") { instance ->
220        val destroyedField = instance["com.mmvm.SomeViewModel", "destroyed"]!!
221        if (destroyedField.value.asBoolean!!) {
222          leakingReasons += "SomeViewModel.destroyed is true"
223        } else {
224          notLeakingReasons += "SomeViewModel.destroyed is false"
225        }
226      }
227    }
228
229    LeakCanary.config = LeakCanary.config.copy(
230        objectInspectors = AndroidObjectInspectors.appDefaults +
231            listOf(addObjectIdLabel, singletonsInspector, mmvmInspector)
232    )
233  }
234}
235```
236
237## Running the LeakCanary analysis in a separate process
238
239LeakCanary 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.
240
241All you have to do is replace the `leakcanary-android` dependency with `leakcanary-android-process`:
242
243```groovy
244dependencies {
245  // debugImplementation 'com.squareup.leakcanary:leakcanary-android:${version}'
246  debugImplementation 'com.squareup.leakcanary:leakcanary-android-process:${version}'
247}
248```
249
250You can call [LeakCanaryProcess.isInAnalyzerProcess](/leakcanary/api/leakcanary/-leak-canary-process/is-in-analyzer-process/) 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.
251
252## Setting up LeakCanary for different product flavors
253
254You can setup LeakCanary to run in a specific product flavors of your app. For example, create:
255
256```
257android {
258  flavorDimensions "default"
259  productFlavors {
260    prod {
261      // ...
262    }
263    qa {
264      // ...
265    }
266    dev {
267      // ...
268    }
269  }
270}
271```
272
273Then, define a custom configuration for the flavor for which you want to enable LeakCanary:
274
275```
276android {
277  // ...
278}
279configurations {
280    devDebugImplementation {}
281}
282```
283
284You can now add the LeakCanary dependency for that configuration:
285
286```
287dependencies {
288  devDebugImplementation "com.squareup.leakcanary:leakcanary-android:${version}"
289}
290```
291
292## Extracting metadata from the heap dump
293
294[LeakCanary.Config.metadataExtractor](/leakcanary/api/leakcanary/-leak-canary/-config/metadata-extractor/) 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.
295
296For 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`.
297
298```kotlin
299class DebugExampleApplication : ExampleApplication() {
300
301  companion object {
302    @JvmStatic
303    lateinit var savedVersionName: String
304  }
305
306  override fun onCreate() {
307    super.onCreate()
308
309    val packageInfo = packageManager.getPackageInfo(packageName, 0)
310    savedVersionName = packageInfo.versionName
311
312    LeakCanary.config = LeakCanary.config.copy(
313        metadataExtractor = MetadataExtractor { graph ->
314          val companionClass =
315            graph.findClassByName("com.example.DebugExampleApplication")!!
316
317          val versionNameField = companionClass["savedVersionName"]!!
318          val versionName = versionNameField.valueAsInstance!!.readAsJavaString()!!
319
320          val defaultMetadata = AndroidMetadataExtractor.extractMetadata(graph)
321
322          mapOf("App Version Name" to versionName) + defaultMetadata
323        })
324  }
325}
326```
327
328## Using LeakCanary with obfuscated apps
329
330If 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.
331
332You have to add a plugin dependency in your root `build.gradle` file:
333
334```groovy
335buildscript {
336  dependencies {
337    classpath 'com.squareup.leakcanary:leakcanary-deobfuscation-gradle-plugin:${version}'
338  }
339}
340```
341
342And then you need to apply and configure the plugin in your app (or library) specific `build.gradle` file:
343
344```groovy
345apply plugin: 'com.android.application'
346apply plugin: 'com.squareup.leakcanary.deobfuscation'
347
348leakCanary {
349  // LeakCanary needs to know which variants have obfuscation turned on
350  filterObfuscatedVariants { variant ->
351    variant.name == "debug"
352  }
353}
354```
355
356Now you can run LeakCanary on an obfuscated app and leak traces will be automatically deobfuscated.
357
358**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.
359
360**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.
361
362## Detecting leaks in JVM applications
363
364While LeakCanary was designed to work out of the box on Android, it can run on any JVM with a bit of configuration.
365
366Add the ObjectWatcher and Shark dependencies to your build file:
367
368```groovy
369dependencies {
370  implementation 'com.squareup.leakcanary:leakcanary-object-watcher:{{ leak_canary.release }}'
371  implementation 'com.squareup.leakcanary:shark:{{ leak_canary.release }}'
372}
373```
374
375Define a `HotSpotHeapDumper` to dump the heap:
376
377```kotlin
378import com.sun.management.HotSpotDiagnosticMXBean
379import java.lang.management.ManagementFactory
380
381object HotSpotHeapDumper {
382  private val mBean: HotSpotDiagnosticMXBean by lazy {
383    val server = ManagementFactory.getPlatformMBeanServer()
384    ManagementFactory.newPlatformMXBeanProxy(
385        server,
386        "com.sun.management:type=HotSpotDiagnostic",
387        HotSpotDiagnosticMXBean::class.java
388    )
389  }
390
391  fun dumpHeap(fileName: String) {
392    mBean.dumpHeap(fileName, LIVE)
393  }
394
395  private const val LIVE = true
396}
397```
398
399Define a `JvmHeapAnalyzer` to analyze the heap when objects are retained and print the result to the console:
400
401```kotlin
402import leakcanary.GcTrigger
403import leakcanary.ObjectWatcher
404import leakcanary.OnObjectRetainedListener
405import java.io.File
406import java.text.SimpleDateFormat
407import java.util.Date
408import java.util.Locale.US
409
410class JvmHeapAnalyzer(private val objectWatcher: ObjectWatcher) :
411    OnObjectRetainedListener {
412
413  private val fileNameFormat = SimpleDateFormat(DATE_PATTERN, US)
414
415  override fun onObjectRetained() {
416    GcTrigger.Default.runGc()
417    if (objectWatcher.retainedObjectCount == 0) {
418      return
419    }
420    val fileName = fileNameFormat.format(Date())
421    val hprofFile = File(fileName)
422
423    println("Dumping the heap to ${hprofFile.absolutePath}")
424    HotSpotHeapDumper.dumpHeap(hprofFile.absolutePath)
425
426    val analyzer = HeapAnalyzer(
427        OnAnalysisProgressListener { step ->
428          println("Analysis in progress, working on: ${step.name}")
429        })
430
431    val heapDumpAnalysis = analyzer.analyze(
432        heapDumpFile = hprofFile,
433        leakingObjectFinder = KeyedWeakReferenceFinder,
434        computeRetainedHeapSize = true,
435        objectInspectors = ObjectInspectors.jdkDefaults
436    )
437    println(heapDumpAnalysis)
438  }
439  companion object {
440    private const val DATE_PATTERN = "yyyy-MM-dd_HH-mm-ss_SSS'.hprof'"
441  }
442}
443```
444
445Create an `ObjectWatcher` instance and configure it to watch objects for 5 seconds before notifying a `JvmHeapAnalyzer` instance:
446
447```kotlin
448val scheduledExecutor = Executors.newSingleThreadScheduledExecutor()
449val objectWatcher = ObjectWatcher(
450    clock = Clock {
451      System.currentTimeMillis()
452    },
453    checkRetainedExecutor = Executor { command ->
454      scheduledExecutor.schedule(command, 5, SECONDS)
455    }
456)
457
458val heapAnalyzer = JvmHeapAnalyzer(objectWatcher)
459objectWatcher.addOnObjectRetainedListener(heapAnalyzer)
460```
461
462Pass objects that you expect to be garbage collected (e.g. closed resources) to the `ObjectWatcher` instance:
463
464```kotlin
465objectWatcher.watch(
466    watchedObject = closedResource,
467    description = "$closedResource is closed and should be garbage collected"
468)
469```
470
471If you end up using LeakCanary on a JVM, the community will definitely benefit from your experience, so don't hesitate to [let us know](https://github.com/square/leakcanary/issues/)!
472
473## PackageManager.getLaunchIntentForPackage() returns LeakLauncherActivity
474
475LeakCanary adds a main activity that has a [Intent#CATEGORY_LAUNCHER](https://developer.android.com/reference/android/content/Intent#CATEGORY_LAUNCHER) category. <a href="https://developer.android.com/reference/android/content/pm/PackageManager#getLaunchIntentForPackage(java.lang.String)">PackageManager.getLaunchIntentForPackage()</a> 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:
476
477* Add `Intent#CATEGORY_INFO` to your main activity intent filter, so that it gets picked up first. This is what the Android documentation recommends.
478* Disable the leakcanary launcher activity by setting the `leak_canary_add_launcher_icon` resource boolean to false.
479
480
481