/
LeakCanary.kt
428 lines (388 loc) · 17.2 KB
/
LeakCanary.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
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,
/**
* Deprecated, add to LeakCanary.config.eventListeners instead.
* Called on a background thread when the heap analysis is complete.
* If you want leaks to be added to the activity that lists leaks, make sure to delegate
* calls to a [DefaultOnHeapAnalyzedListener].
*
* Defaults to [DefaultOnHeapAnalyzedListener]
*/
@Deprecated(message = "Add to LeakCanary.config.eventListeners instead")
val onHeapAnalyzedListener: OnHeapAnalyzedListener = DefaultOnHeapAnalyzedListener.create(),
/**
* 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,
/**
* Deprecated: This is a no-op, set a custom [leakingObjectFinder] instead.
*/
@Deprecated("This is a no-op, set a custom leakingObjectFinder instead")
val useExperimentalLeakFinders: Boolean = false
) {
/**
* 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 onHeapAnalyzedListener = config.onHeapAnalyzedListener
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 useExperimentalLeakFinders = config.useExperimentalLeakFinders
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.onHeapAnalyzedListener] */
@Deprecated(message = "Add to LeakCanary.config.eventListeners instead")
fun onHeapAnalyzedListener(onHeapAnalyzedListener: OnHeapAnalyzedListener) =
apply { this.onHeapAnalyzedListener = onHeapAnalyzedListener }
/** @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.useExperimentalLeakFinders] */
@Deprecated("Set a custom leakingObjectFinder instead")
fun useExperimentalLeakFinders(useExperimentalLeakFinders: Boolean) =
apply { this.useExperimentalLeakFinders = useExperimentalLeakFinders }
/** @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,
onHeapAnalyzedListener = onHeapAnalyzedListener,
metadataExtractor = metadataExtractor,
computeRetainedHeapSize = computeRetainedHeapSize,
maxStoredHeapDumps = maxStoredHeapDumps,
requestWriteExternalStoragePermission = requestWriteExternalStoragePermission,
leakingObjectFinder = leakingObjectFinder,
heapDumper = heapDumper,
eventListeners = eventListeners,
useExperimentalLeakFinders = useExperimentalLeakFinders,
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)
}