1 /*
<lambda>null2 * Copyright (C) 2018 Square, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package shark
17
18 import java.util.EnumSet
19 import kotlin.math.absoluteValue
20 import shark.AndroidObjectInspectors.Companion.appDefaults
21 import shark.AndroidServices.aliveAndroidServiceObjectIds
22 import shark.FilteringLeakingObjectFinder.LeakingObjectFilter
23 import shark.HeapObject.HeapInstance
24 import shark.internal.InternalSharkCollectionsHelper
25
26 /**
27 * A set of default [ObjectInspector]s that knows about common AOSP and library
28 * classes.
29 *
30 * These are heuristics based on our experience and knowledge of AOSP and various library
31 * internals. We only make a decision if we're reasonably sure the state of an object is
32 * unlikely to be the result of a programmer mistake.
33 *
34 * For example, no matter how many mistakes we make in our code, the value of Activity.mDestroy
35 * will not be influenced by those mistakes.
36 *
37 * Most developers should use the entire set of default [ObjectInspector] by calling [appDefaults],
38 * unless there's a bug and you temporarily want to remove an inspector.
39 */
40 enum class AndroidObjectInspectors : ObjectInspector {
41
42 VIEW {
43 override val leakingObjectFilter = { heapObject: HeapObject ->
44 if (heapObject is HeapInstance && heapObject instanceOf "android.view.View") {
45 // Leaking if null parent or non view parent.
46 val viewParent = heapObject["android.view.View", "mParent"]!!.valueAsInstance
47 val isParentlessView = viewParent == null
48 val isChildOfViewRootImpl =
49 viewParent != null && !(viewParent instanceOf "android.view.View")
50 val isRootView = isParentlessView || isChildOfViewRootImpl
51
52 // This filter only cares for root view because we only need one view in a view hierarchy.
53 if (isRootView) {
54 val mContext = heapObject["android.view.View", "mContext"]!!.value.asObject!!.asInstance!!
55 val activityContext = mContext.unwrapActivityContext()
56 val mContextIsDestroyedActivity = (activityContext != null &&
57 activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true)
58 if (mContextIsDestroyedActivity) {
59 // Root view with unwrapped mContext a destroyed activity.
60 true
61 } else {
62 val viewDetached =
63 heapObject["android.view.View", "mAttachInfo"]!!.value.isNullReference
64 if (viewDetached) {
65 val mWindowAttachCount =
66 heapObject["android.view.View", "mWindowAttachCount"]?.value!!.asInt!!
67 if (mWindowAttachCount > 0) {
68 when {
69 isChildOfViewRootImpl -> {
70 // Child of ViewRootImpl that was once attached and is now detached.
71 // Unwrapped mContext not a destroyed activity. This could be a dialog root.
72 true
73 }
74 heapObject.instanceClassName == "com.android.internal.policy.DecorView" -> {
75 // DecorView with null parent, once attached now detached.
76 // Unwrapped mContext not a destroyed activity. This could be a dialog root.
77 // Unlikely to be a reusable cached view => leak.
78 true
79 }
80 else -> {
81 // View with null parent, once attached now detached.
82 // Unwrapped mContext not a destroyed activity. This could be a dialog root.
83 // Could be a leak or could be a reusable cached view.
84 false
85 }
86 }
87 } else {
88 // Root view, detached but was never attached.
89 // This could be a cached instance.
90 false
91 }
92 } else {
93 // Root view that is attached.
94 false
95 }
96 }
97 } else {
98 // Not a root view.
99 false
100 }
101 } else {
102 // Not a view
103 false
104 }
105 }
106
107 override fun inspect(
108 reporter: ObjectReporter
109 ) {
110 reporter.whenInstanceOf("android.view.View") { instance ->
111 // This skips edge cases like Toast$TN.mNextView holding on to an unattached and unparented
112 // next toast view
113 var rootParent = instance["android.view.View", "mParent"]!!.valueAsInstance
114 var rootView: HeapInstance? = null
115 while (rootParent != null && rootParent instanceOf "android.view.View") {
116 rootView = rootParent
117 rootParent = rootParent["android.view.View", "mParent"]!!.valueAsInstance
118 }
119
120 val partOfWindowHierarchy = rootParent != null || (rootView != null &&
121 rootView.instanceClassName == "com.android.internal.policy.DecorView")
122
123 val mWindowAttachCount =
124 instance["android.view.View", "mWindowAttachCount"]?.value!!.asInt!!
125 val viewDetached = instance["android.view.View", "mAttachInfo"]!!.value.isNullReference
126 val mContext = instance["android.view.View", "mContext"]!!.value.asObject!!.asInstance!!
127
128 val activityContext = mContext.unwrapActivityContext()
129 if (activityContext != null && activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true) {
130 leakingReasons += "View.mContext references a destroyed activity"
131 } else {
132 if (partOfWindowHierarchy && mWindowAttachCount > 0) {
133 if (viewDetached) {
134 leakingReasons += "View detached yet still part of window view hierarchy"
135 } else {
136 if (rootView != null && rootView["android.view.View", "mAttachInfo"]!!.value.isNullReference) {
137 leakingReasons += "View attached but root view ${rootView.instanceClassName} detached (attach disorder)"
138 } else {
139 notLeakingReasons += "View attached"
140 }
141 }
142 }
143 }
144
145 labels += if (partOfWindowHierarchy) {
146 "View is part of a window view hierarchy"
147 } else {
148 "View not part of a window view hierarchy"
149 }
150
151 labels += if (viewDetached) {
152 "View.mAttachInfo is null (view detached)"
153 } else {
154 "View.mAttachInfo is not null (view attached)"
155 }
156
157 AndroidResourceIdNames.readFromHeap(instance.graph)
158 ?.let { resIds ->
159 val mID = instance["android.view.View", "mID"]!!.value.asInt!!
160 val noViewId = -1
161 if (mID != noViewId) {
162 val resourceName = resIds[mID]
163 labels += "View.mID = R.id.$resourceName"
164 }
165 }
166 labels += "View.mWindowAttachCount = $mWindowAttachCount"
167 }
168 }
169 },
170
171 EDITOR {
172 override val leakingObjectFilter = { heapObject: HeapObject ->
173 heapObject is HeapInstance &&
174 heapObject instanceOf "android.widget.Editor" &&
175 heapObject["android.widget.Editor", "mTextView"]?.value?.asObject?.let { textView ->
176 VIEW.leakingObjectFilter!!(textView)
177 } ?: false
178 }
179
180 override fun inspect(reporter: ObjectReporter) {
181 reporter.whenInstanceOf("android.widget.Editor") { instance ->
182 applyFromField(VIEW, instance["android.widget.Editor", "mTextView"])
183 }
184 }
185 },
186
187 ACTIVITY {
188 override val leakingObjectFilter = { heapObject: HeapObject ->
189 heapObject is HeapInstance &&
190 heapObject instanceOf "android.app.Activity" &&
191 heapObject["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true
192 }
193
194 override fun inspect(
195 reporter: ObjectReporter
196 ) {
197 reporter.whenInstanceOf("android.app.Activity") { instance ->
198 // Activity.mDestroyed was introduced in 17.
199 // https://android.googlesource.com/platform/frameworks/base/+
200 // /6d9dcbccec126d9b87ab6587e686e28b87e5a04d
201 val field = instance["android.app.Activity", "mDestroyed"]
202
203 if (field != null) {
204 if (field.value.asBoolean!!) {
205 leakingReasons += field describedWithValue "true"
206 } else {
207 notLeakingReasons += field describedWithValue "false"
208 }
209 }
210 }
211 }
212 },
213
214 SERVICE {
215 override val leakingObjectFilter = { heapObject: HeapObject ->
216 heapObject is HeapInstance &&
217 heapObject instanceOf "android.app.Service" &&
218 heapObject.objectId !in heapObject.graph.aliveAndroidServiceObjectIds
219 }
220
221 override fun inspect(
222 reporter: ObjectReporter
223 ) {
224 reporter.whenInstanceOf("android.app.Service") { instance ->
225 if (instance.objectId in instance.graph.aliveAndroidServiceObjectIds) {
226 notLeakingReasons += "Service held by ActivityThread"
227 } else {
228 leakingReasons += "Service not held by ActivityThread"
229 }
230 }
231 }
232 },
233
234 CONTEXT_FIELD {
235 override fun inspect(reporter: ObjectReporter) {
236 val instance = reporter.heapObject
237 if (instance !is HeapInstance) {
238 return
239 }
240 instance.readFields().forEach { field ->
241 val fieldInstance = field.valueAsInstance
242 if (fieldInstance != null && fieldInstance instanceOf "android.content.Context") {
243 reporter.run {
244 val componentContext = fieldInstance.unwrapComponentContext()
245 labels += if (componentContext == null) {
246 "${field.name} instance of ${fieldInstance.instanceClassName}"
247 } else if (componentContext instanceOf "android.app.Activity") {
248 val activityDescription =
249 "with mDestroyed = " + (componentContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean?.toString()
250 ?: "UNKNOWN")
251 if (componentContext == fieldInstance) {
252 "${field.name} instance of ${fieldInstance.instanceClassName} $activityDescription"
253 } else {
254 "${field.name} instance of ${fieldInstance.instanceClassName}, " +
255 "wrapping activity ${componentContext.instanceClassName} $activityDescription"
256 }
257 } else if (componentContext == fieldInstance) {
258 // No need to add "instance of Application / Service", devs know their own classes.
259 "${field.name} instance of ${fieldInstance.instanceClassName}"
260 } else {
261 "${field.name} instance of ${fieldInstance.instanceClassName}, wrapping ${componentContext.instanceClassName}"
262 }
263 }
264 }
265 }
266 }
267 },
268
269 CONTEXT_WRAPPER {
270
271 override val leakingObjectFilter = { heapObject: HeapObject ->
272 heapObject is HeapInstance &&
273 heapObject.unwrapActivityContext()
274 ?.get("android.app.Activity", "mDestroyed")?.value?.asBoolean == true
275 }
276
277 override fun inspect(
278 reporter: ObjectReporter
279 ) {
280 val instance = reporter.heapObject
281 if (instance !is HeapInstance) {
282 return
283 }
284
285 // We're looking for ContextWrapper instances that are not Activity, Application or Service.
286 // So we stop whenever we find any of those 4 classes, and then only keep ContextWrapper.
287 val matchingClassName = instance.instanceClass.classHierarchy.map { it.name }
288 .firstOrNull {
289 when (it) {
290 "android.content.ContextWrapper",
291 "android.app.Activity",
292 "android.app.Application",
293 "android.app.Service"
294 -> true
295 else -> false
296 }
297 }
298
299 if (matchingClassName == "android.content.ContextWrapper") {
300 reporter.run {
301 val componentContext = instance.unwrapComponentContext()
302 if (componentContext != null) {
303 if (componentContext instanceOf "android.app.Activity") {
304 val mDestroyed = componentContext["android.app.Activity", "mDestroyed"]
305 if (mDestroyed != null) {
306 if (mDestroyed.value.asBoolean!!) {
307 leakingReasons += "${instance.instanceClassSimpleName} wraps an Activity with Activity.mDestroyed true"
308 } else {
309 // We can't assume it's not leaking, because this context might have a shorter lifecycle
310 // than the activity. So we'll just add a label.
311 labels += "${instance.instanceClassSimpleName} wraps an Activity with Activity.mDestroyed false"
312 }
313 }
314 } else if (componentContext instanceOf "android.app.Application") {
315 labels += "${instance.instanceClassSimpleName} wraps an Application context"
316 } else {
317 labels += "${instance.instanceClassSimpleName} wraps a Service context"
318 }
319 } else {
320 labels += "${instance.instanceClassSimpleName} does not wrap a known Android context"
321 }
322 }
323 }
324 }
325 },
326
327 APPLICATION_PACKAGE_MANAGER {
328 override val leakingObjectFilter = { heapObject: HeapObject ->
329 heapObject is HeapInstance &&
330 heapObject instanceOf "android.app.ApplicationContextManager" &&
331 heapObject["android.app.ApplicationContextManager", "mContext"]!!
332 .valueAsInstance!!.outerContextIsLeaking()
333 }
334
335 override fun inspect(reporter: ObjectReporter) {
336 reporter.whenInstanceOf("android.app.ApplicationContextManager") { instance ->
337 val outerContext = instance["android.app.ApplicationContextManager", "mContext"]!!
338 .valueAsInstance!!["android.app.ContextImpl", "mOuterContext"]!!
339 .valueAsInstance!!
340 inspectContextImplOuterContext(outerContext, instance, "ApplicationContextManager.mContext")
341 }
342 }
343 },
344
345 CONTEXT_IMPL {
346 override val leakingObjectFilter = { heapObject: HeapObject ->
347 heapObject is HeapInstance &&
348 heapObject instanceOf "android.app.ContextImpl" &&
349 heapObject.outerContextIsLeaking()
350 }
351
352 override fun inspect(reporter: ObjectReporter) {
353 reporter.whenInstanceOf("android.app.ContextImpl") { instance ->
354 val outerContext = instance["android.app.ContextImpl", "mOuterContext"]!!
355 .valueAsInstance!!
356 inspectContextImplOuterContext(outerContext, instance)
357 }
358 }
359 },
360
361 DIALOG {
362 override fun inspect(
363 reporter: ObjectReporter
364 ) {
365 reporter.whenInstanceOf("android.app.Dialog") { instance ->
366 val mDecor = instance["android.app.Dialog", "mDecor"]!!
367 // Can't infer leaking status: mDecor null means either never shown or dismiss.
368 // mDecor non null means the dialog is showing, but sometimes dialogs stay showing
369 // after activity destroyed so that's not really a non leak either.
370 labels += mDecor describedWithValue if (mDecor.value.isNullReference) {
371 "null"
372 } else {
373 "not null"
374 }
375 }
376 }
377 },
378
379 ACTIVITY_THREAD {
380 override fun inspect(reporter: ObjectReporter) {
381 reporter.whenInstanceOf("android.app.ActivityThread") {
382 notLeakingReasons += "ActivityThread is a singleton"
383 }
384 }
385 },
386
387 APPLICATION {
388 override fun inspect(
389 reporter: ObjectReporter
390 ) {
391 reporter.whenInstanceOf("android.app.Application") {
392 notLeakingReasons += "Application is a singleton"
393 }
394 }
395 },
396
397 INPUT_METHOD_MANAGER {
398 override fun inspect(
399 reporter: ObjectReporter
400 ) {
401 reporter.whenInstanceOf("android.view.inputmethod.InputMethodManager") {
402 notLeakingReasons += "InputMethodManager is a singleton"
403 }
404 }
405 },
406
407 FRAGMENT {
408 override val leakingObjectFilter = { heapObject: HeapObject ->
409 heapObject is HeapInstance &&
410 heapObject instanceOf "android.app.Fragment" &&
411 heapObject["android.app.Fragment", "mFragmentManager"]!!.value.isNullReference
412 }
413
414 override fun inspect(
415 reporter: ObjectReporter
416 ) {
417 reporter.whenInstanceOf("android.app.Fragment") { instance ->
418 val fragmentManager = instance["android.app.Fragment", "mFragmentManager"]!!
419 if (fragmentManager.value.isNullReference) {
420 leakingReasons += fragmentManager describedWithValue "null"
421 } else {
422 notLeakingReasons += fragmentManager describedWithValue "not null"
423 }
424 val mTag = instance["android.app.Fragment", "mTag"]?.value?.readAsJavaString()
425 if (!mTag.isNullOrEmpty()) {
426 labels += "Fragment.mTag=$mTag"
427 }
428 }
429 }
430 },
431
432 SUPPORT_FRAGMENT {
433
434 override val leakingObjectFilter = { heapObject: HeapObject ->
435 heapObject is HeapInstance &&
436 heapObject instanceOf ANDROID_SUPPORT_FRAGMENT_CLASS_NAME &&
437 heapObject.getOrThrow(
438 ANDROID_SUPPORT_FRAGMENT_CLASS_NAME, "mFragmentManager"
439 ).value.isNullReference
440 }
441
442 override fun inspect(
443 reporter: ObjectReporter
444 ) {
445 reporter.whenInstanceOf(ANDROID_SUPPORT_FRAGMENT_CLASS_NAME) { instance ->
446 val fragmentManager =
447 instance.getOrThrow(ANDROID_SUPPORT_FRAGMENT_CLASS_NAME, "mFragmentManager")
448 if (fragmentManager.value.isNullReference) {
449 leakingReasons += fragmentManager describedWithValue "null"
450 } else {
451 notLeakingReasons += fragmentManager describedWithValue "not null"
452 }
453 val mTag = instance[ANDROID_SUPPORT_FRAGMENT_CLASS_NAME, "mTag"]?.value?.readAsJavaString()
454 if (!mTag.isNullOrEmpty()) {
455 labels += "Fragment.mTag=$mTag"
456 }
457 }
458 }
459 },
460
461 ANDROIDX_FRAGMENT {
462 override val leakingObjectFilter = { heapObject: HeapObject ->
463 heapObject is HeapInstance &&
464 heapObject instanceOf "androidx.fragment.app.Fragment" &&
465 heapObject["androidx.fragment.app.Fragment", "mLifecycleRegistry"]!!
466 .valueAsInstance
467 ?.lifecycleRegistryState == "DESTROYED"
468 }
469
470 override fun inspect(
471 reporter: ObjectReporter
472 ) {
473 reporter.whenInstanceOf("androidx.fragment.app.Fragment") { instance ->
474 val lifecycleRegistryField = instance["androidx.fragment.app.Fragment", "mLifecycleRegistry"]!!
475 val lifecycleRegistry = lifecycleRegistryField.valueAsInstance
476 if (lifecycleRegistry != null) {
477 val state = lifecycleRegistry.lifecycleRegistryState
478 val reason = "Fragment.mLifecycleRegistry.state is $state"
479 if (state == "DESTROYED") {
480 leakingReasons += reason
481 } else {
482 notLeakingReasons += reason
483 }
484 } else {
485 labels += "Fragment.mLifecycleRegistry = null"
486 }
487 val mTag = instance["androidx.fragment.app.Fragment", "mTag"]?.value?.readAsJavaString()
488 if (!mTag.isNullOrEmpty()) {
489 labels += "Fragment.mTag = $mTag"
490 }
491 }
492 }
493 },
494
495 MESSAGE_QUEUE {
496 override val leakingObjectFilter = { heapObject: HeapObject ->
497 heapObject is HeapInstance &&
498 heapObject instanceOf "android.os.MessageQueue" &&
499 (heapObject["android.os.MessageQueue", "mQuitting"]
500 ?: heapObject["android.os.MessageQueue", "mQuiting"]!!).value.asBoolean!!
501 }
502
503 override fun inspect(
504 reporter: ObjectReporter
505 ) {
506 reporter.whenInstanceOf("android.os.MessageQueue") { instance ->
507 // mQuiting had a typo and was renamed to mQuitting
508 // https://android.googlesource.com/platform/frameworks/base/+/013cf847bcfd2828d34dced60adf2d3dd98021dc
509 val mQuitting = instance["android.os.MessageQueue", "mQuitting"]
510 ?: instance["android.os.MessageQueue", "mQuiting"]!!
511 if (mQuitting.value.asBoolean!!) {
512 leakingReasons += mQuitting describedWithValue "true"
513 } else {
514 notLeakingReasons += mQuitting describedWithValue "false"
515 }
516
517 val queueHead = instance["android.os.MessageQueue", "mMessages"]!!.valueAsInstance
518 if (queueHead != null) {
519 val targetHandler = queueHead["android.os.Message", "target"]!!.valueAsInstance
520 if (targetHandler != null) {
521 val looper = targetHandler["android.os.Handler", "mLooper"]!!.valueAsInstance
522 if (looper != null) {
523 val thread = looper["android.os.Looper", "mThread"]!!.valueAsInstance!!
524 val threadName = thread[Thread::class, "name"]!!.value.readAsJavaString()
525 labels += "HandlerThread: \"$threadName\""
526 }
527 }
528 }
529 }
530 }
531 },
532
533 LOADED_APK {
534 override fun inspect(
535 reporter: ObjectReporter
536 ) {
537 reporter.whenInstanceOf("android.app.LoadedApk") { instance ->
538 val receiversMap = instance["android.app.LoadedApk", "mReceivers"]!!.valueAsInstance!!
539 val receiversArray = receiversMap["android.util.ArrayMap", "mArray"]!!.valueAsObjectArray!!
540 val receiverContextList = receiversArray.readElements().toList()
541
542 val allReceivers = (receiverContextList.indices step 2).mapNotNull { index ->
543 val context = receiverContextList[index]
544 if (context.isNonNullReference) {
545 val contextReceiversMap = receiverContextList[index + 1].asObject!!.asInstance!!
546 val contextReceivers = contextReceiversMap["android.util.ArrayMap", "mArray"]!!
547 .valueAsObjectArray!!
548 .readElements()
549 .toList()
550
551 val receivers =
552 (contextReceivers.indices step 2).mapNotNull { contextReceivers[it].asObject?.asInstance }
553 val contextInstance = context.asObject!!.asInstance!!
554 val contextString =
555 "${contextInstance.instanceClassSimpleName}@${contextInstance.objectId}"
556 contextString to receivers.map { "${it.instanceClassSimpleName}@${it.objectId}" }
557 } else {
558 null
559 }
560 }.toList()
561
562 if (allReceivers.isNotEmpty()) {
563 labels += "Receivers"
564 allReceivers.forEach { (contextString, receiverStrings) ->
565 labels += "..$contextString"
566 receiverStrings.forEach { receiverString ->
567 labels += "....$receiverString"
568 }
569 }
570 }
571 }
572 }
573 },
574
575 MORTAR_PRESENTER {
576 override fun inspect(
577 reporter: ObjectReporter
578 ) {
579 reporter.whenInstanceOf("mortar.Presenter") { instance ->
580 val view = instance.getOrThrow("mortar.Presenter", "view")
581 labels += view describedWithValue if (view.value.isNullReference) "null" else "not null"
582 }
583 }
584 },
585
586 MORTAR_SCOPE {
587 override val leakingObjectFilter = { heapObject: HeapObject ->
588 heapObject is HeapInstance &&
589 heapObject instanceOf "mortar.MortarScope" &&
590 heapObject.getOrThrow("mortar.MortarScope", "dead").value.asBoolean!!
591 }
592
593 override fun inspect(reporter: ObjectReporter) {
594 reporter.whenInstanceOf("mortar.MortarScope") { instance ->
595 val dead = instance.getOrThrow("mortar.MortarScope", "dead").value.asBoolean!!
596 val scopeName = instance.getOrThrow("mortar.MortarScope", "name").value.readAsJavaString()
597 if (dead) {
598 leakingReasons += "mortar.MortarScope.dead is true for scope $scopeName"
599 } else {
600 notLeakingReasons += "mortar.MortarScope.dead is false for scope $scopeName"
601 }
602 }
603 }
604 },
605
606 COORDINATOR {
607 override fun inspect(
608 reporter: ObjectReporter
609 ) {
610 reporter.whenInstanceOf("com.squareup.coordinators.Coordinator") { instance ->
611 val attached = instance.getOrThrow("com.squareup.coordinators.Coordinator", "attached")
612 labels += attached describedWithValue "${attached.value.asBoolean}"
613 }
614 }
615 },
616
617 MAIN_THREAD {
618 override fun inspect(
619 reporter: ObjectReporter
620 ) {
621 reporter.whenInstanceOf(Thread::class) { instance ->
622 val threadName = instance[Thread::class, "name"]!!.value.readAsJavaString()
623 if (threadName == "main") {
624 notLeakingReasons += "the main thread always runs"
625 }
626 }
627 }
628 },
629
630 VIEW_ROOT_IMPL {
631 override val leakingObjectFilter = { heapObject: HeapObject ->
632 if (heapObject is HeapInstance &&
633 heapObject instanceOf "android.view.ViewRootImpl"
634 ) {
635 if (heapObject["android.view.ViewRootImpl", "mView"]!!.value.isNullReference) {
636 true
637 } else {
638 val mContextField = heapObject["android.view.ViewRootImpl", "mContext"]
639 if (mContextField != null) {
640 val mContext = mContextField.valueAsInstance!!
641 val activityContext = mContext.unwrapActivityContext()
642 (activityContext != null && activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true)
643 } else {
644 false
645 }
646 }
647 } else {
648 false
649 }
650 }
651
652 override fun inspect(reporter: ObjectReporter) {
653 reporter.whenInstanceOf("android.view.ViewRootImpl") { instance ->
654 val mViewField = instance["android.view.ViewRootImpl", "mView"]!!
655 if (mViewField.value.isNullReference) {
656 leakingReasons += mViewField describedWithValue "null"
657 } else {
658 // ViewRootImpl.mContext wasn't always here.
659 val mContextField = instance["android.view.ViewRootImpl", "mContext"]
660 if (mContextField != null) {
661 val mContext = mContextField.valueAsInstance!!
662 val activityContext = mContext.unwrapActivityContext()
663 if (activityContext != null && activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true) {
664 leakingReasons += "ViewRootImpl.mContext references a destroyed activity, did you forget to cancel toasts or dismiss dialogs?"
665 }
666 }
667 labels += mViewField describedWithValue "not null"
668 }
669 val mWindowAttributes =
670 instance["android.view.ViewRootImpl", "mWindowAttributes"]!!.valueAsInstance!!
671 val mTitleField = mWindowAttributes["android.view.WindowManager\$LayoutParams", "mTitle"]!!
672 labels += if (mTitleField.value.isNonNullReference) {
673 val mTitle =
674 mTitleField.valueAsInstance!!.readAsJavaString()!!
675 "mWindowAttributes.mTitle = \"$mTitle\""
676 } else {
677 "mWindowAttributes.mTitle is null"
678 }
679
680 val type =
681 mWindowAttributes["android.view.WindowManager\$LayoutParams", "type"]!!.value.asInt!!
682 // android.view.WindowManager.LayoutParams.TYPE_TOAST
683 val details = if (type == 2005) {
684 " (Toast)"
685 } else ""
686 labels += "mWindowAttributes.type = $type$details"
687 }
688 }
689 },
690
691 WINDOW {
692 override val leakingObjectFilter = { heapObject: HeapObject ->
693 heapObject is HeapInstance &&
694 heapObject instanceOf "android.view.Window" &&
695 heapObject["android.view.Window", "mDestroyed"]!!.value.asBoolean!!
696 }
697
698 override fun inspect(
699 reporter: ObjectReporter
700 ) {
701 reporter.whenInstanceOf("android.view.Window") { instance ->
702 val mDestroyed = instance["android.view.Window", "mDestroyed"]!!
703
704 if (mDestroyed.value.asBoolean!!) {
705 leakingReasons += mDestroyed describedWithValue "true"
706 } else {
707 // A dialog window could be leaking, destroy is only set to false for activity windows.
708 labels += mDestroyed describedWithValue "false"
709 }
710 }
711 }
712 },
713
714 MESSAGE {
715 override fun inspect(reporter: ObjectReporter) {
716 reporter.whenInstanceOf("android.os.Message") { instance ->
717 labels += "Message.what = ${instance["android.os.Message", "what"]!!.value.asInt}"
718
719 val heapDumpUptimeMillis = KeyedWeakReferenceFinder.heapDumpUptimeMillis(instance.graph)
720 val whenUptimeMillis = instance["android.os.Message", "when"]!!.value.asLong!!
721
722 labels += if (heapDumpUptimeMillis != null) {
723 val diffMs = whenUptimeMillis - heapDumpUptimeMillis
724 if (diffMs > 0) {
725 "Message.when = $whenUptimeMillis ($diffMs ms after heap dump)"
726 } else {
727 "Message.when = $whenUptimeMillis (${diffMs.absoluteValue} ms before heap dump)"
728 }
729 } else {
730 "Message.when = $whenUptimeMillis"
731 }
732
733 labels += "Message.obj = ${instance["android.os.Message", "obj"]!!.value.asObject}"
734 labels += "Message.callback = ${instance["android.os.Message", "callback"]!!.value.asObject}"
735 labels += "Message.target = ${instance["android.os.Message", "target"]!!.value.asObject}"
736 }
737 }
738 },
739
740 TOAST {
741 override val leakingObjectFilter = { heapObject: HeapObject ->
742 if (heapObject is HeapInstance && heapObject instanceOf "android.widget.Toast") {
743 val tnInstance =
744 heapObject["android.widget.Toast", "mTN"]!!.value.asObject!!.asInstance!!
745 (tnInstance["android.widget.Toast\$TN", "mWM"]!!.value.isNonNullReference &&
746 tnInstance["android.widget.Toast\$TN", "mView"]!!.value.isNullReference)
747 } else false
748 }
749
750 override fun inspect(
751 reporter: ObjectReporter
752 ) {
753 reporter.whenInstanceOf("android.widget.Toast") { instance ->
754 val tnInstance =
755 instance["android.widget.Toast", "mTN"]!!.value.asObject!!.asInstance!!
756 // mWM is set in android.widget.Toast.TN#handleShow and never unset, so this toast was never
757 // shown, we don't know if it's leaking.
758 if (tnInstance["android.widget.Toast\$TN", "mWM"]!!.value.isNonNullReference) {
759 // mView is reset to null in android.widget.Toast.TN#handleHide
760 if (tnInstance["android.widget.Toast\$TN", "mView"]!!.value.isNullReference) {
761 leakingReasons += "This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null)"
762 } else {
763 notLeakingReasons += "This toast is showing (Toast.mTN.mWM != null && Toast.mTN.mView != null)"
764 }
765 }
766 }
767 }
768 },
769
770 RECOMPOSER {
771 override fun inspect(reporter: ObjectReporter) {
772 reporter.whenInstanceOf("androidx.compose.runtime.Recomposer") { instance ->
773 val stateFlow =
774 instance["androidx.compose.runtime.Recomposer", "_state"]!!.valueAsInstance!!
775 val state = stateFlow["kotlinx.coroutines.flow.StateFlowImpl", "_state"]?.valueAsInstance
776 if (state != null) {
777 val stateName = state["java.lang.Enum", "name"]!!.valueAsInstance!!.readAsJavaString()!!
778 val label = "Recomposer is in state $stateName"
779 when (stateName) {
780 "ShutDown", "ShuttingDown" -> leakingReasons += label
781 "Inactive", "InactivePendingWork" -> labels += label
782 "PendingWork", "Idle" -> notLeakingReasons += label
783 }
784 }
785 }
786 }
787 },
788
789 COMPOSITION_IMPL {
790 override fun inspect(reporter: ObjectReporter) {
791 reporter.whenInstanceOf("androidx.compose.runtime.CompositionImpl") { instance ->
792 if (instance["androidx.compose.runtime.CompositionImpl", "disposed"]!!.value.asBoolean!!) {
793 leakingReasons += "Composition disposed"
794 } else {
795 notLeakingReasons += "Composition not disposed"
796 }
797 }
798 }
799 },
800
801 ANIMATOR {
802 override fun inspect(reporter: ObjectReporter) {
803 reporter.whenInstanceOf("android.animation.Animator") { instance ->
804 val mListeners = instance["android.animation.Animator", "mListeners"]!!.valueAsInstance
805 if (mListeners != null) {
806 val listenerValues = InternalSharkCollectionsHelper.arrayListValues(mListeners).toList()
807 if (listenerValues.isNotEmpty()) {
808 listenerValues.forEach { value ->
809 labels += "mListeners$value"
810 }
811 } else {
812 labels += "mListeners is empty"
813 }
814 } else {
815 labels += "mListeners = null"
816 }
817 }
818 }
819 },
820
821 OBJECT_ANIMATOR {
822 override fun inspect(reporter: ObjectReporter) {
823 reporter.whenInstanceOf("android.animation.ObjectAnimator") { instance ->
824 labels += "mPropertyName = " + (instance["android.animation.ObjectAnimator", "mPropertyName"]!!.valueAsInstance?.readAsJavaString()
825 ?: "null")
826 val mProperty = instance["android.animation.ObjectAnimator", "mProperty"]!!.valueAsInstance
827 if (mProperty == null) {
828 labels += "mProperty = null"
829 } else {
830 labels += "mProperty.mName = " + (mProperty["android.util.Property", "mName"]!!.valueAsInstance?.readAsJavaString()
831 ?: "null")
832 labels += "mProperty.mType = " + (mProperty["android.util.Property", "mType"]!!.valueAsClass?.name
833 ?: "null")
834 }
835 labels += "mInitialized = " + instance["android.animation.ValueAnimator", "mInitialized"]!!.value.asBoolean!!
836 labels += "mStarted = " + instance["android.animation.ValueAnimator", "mStarted"]!!.value.asBoolean!!
837 labels += "mRunning = " + instance["android.animation.ValueAnimator", "mRunning"]!!.value.asBoolean!!
838 labels += "mAnimationEndRequested = " + instance["android.animation.ValueAnimator", "mAnimationEndRequested"]!!.value.asBoolean!!
839 labels += "mDuration = " + instance["android.animation.ValueAnimator", "mDuration"]!!.value.asLong!!
840 labels += "mStartDelay = " + instance["android.animation.ValueAnimator", "mStartDelay"]!!.value.asLong!!
841 val repeatCount = instance["android.animation.ValueAnimator", "mRepeatCount"]!!.value.asInt!!
842 labels += "mRepeatCount = " + if (repeatCount == -1) "INFINITE (-1)" else repeatCount
843
844 val repeatModeConstant = when (val repeatMode =
845 instance["android.animation.ValueAnimator", "mRepeatMode"]!!.value.asInt!!) {
846 1 -> "RESTART (1)"
847 2 -> "REVERSE (2)"
848 else -> "Unknown ($repeatMode)"
849 }
850 labels += "mRepeatMode = $repeatModeConstant"
851 }
852 }
853 },
854
855 LIFECYCLE_REGISTRY {
856 override fun inspect(reporter: ObjectReporter) {
857 reporter.whenInstanceOf("androidx.lifecycle.LifecycleRegistry") { instance ->
858 val state = instance.lifecycleRegistryState
859 // If state is DESTROYED, this doesn't mean the LifecycleRegistry itself is leaking.
860 // Fragment.mViewLifecycleRegistry becomes DESTROYED when the fragment view is destroyed,
861 // but the registry itself is still held in memory by the fragment.
862 if (state != "DESTROYED") {
863 notLeakingReasons += "state is $state"
864 } else {
865 labels += "state = $state"
866 }
867 }
868 }
869 },
870
871 STUB {
872 override fun inspect(reporter: ObjectReporter) {
873 reporter.whenInstanceOf("android.os.Binder") { instance ->
874 labels + "${instance.instanceClassSimpleName} is a binder stub. Binder stubs will often be" +
875 " retained long after the associated activity or service is destroyed, as by design stubs" +
876 " are retained until the other side gets GCed. If ${instance.instanceClassSimpleName} is" +
877 " not a *static* inner class then that's most likely the root cause of this leak. Make" +
878 " it static. If ${instance.instanceClassSimpleName} is an Android Framework class, file" +
879 " a ticket here: https://issuetracker.google.com/issues/new?component=192705"
880 }
881 }
882 },
883 ;
884
885 internal open val leakingObjectFilter: ((heapObject: HeapObject) -> Boolean)? = null
886
887 companion object {
888 /** @see AndroidObjectInspectors */
889 val appDefaults: List<ObjectInspector>
890 get() = ObjectInspectors.jdkDefaults + values()
891
892 /**
893 * Returns a list of [LeakingObjectFilter] suitable for apps.
894 */
895 val appLeakingObjectFilters: List<LeakingObjectFilter> =
896 ObjectInspectors.jdkLeakingObjectFilters +
897 createLeakingObjectFilters(EnumSet.allOf(AndroidObjectInspectors::class.java))
898
899 /**
900 * Creates a list of [LeakingObjectFilter] based on the passed in [AndroidObjectInspectors].
901 */
902 fun createLeakingObjectFilters(inspectors: Set<AndroidObjectInspectors>): List<LeakingObjectFilter> =
903 inspectors.mapNotNull { it.leakingObjectFilter }
904 .map { filter ->
905 LeakingObjectFilter { heapObject -> filter(heapObject) }
906 }
907 }
908
909 // Using a string builder to prevent Jetifier from changing this string to Android X Fragment
910 @Suppress("VariableNaming", "PropertyName")
911 internal val ANDROID_SUPPORT_FRAGMENT_CLASS_NAME =
912 StringBuilder("android.").append("support.v4.app.Fragment")
913 .toString()
914 }
915
outerContextIsLeakingnull916 private fun HeapInstance.outerContextIsLeaking() =
917 this["android.app.ContextImpl", "mOuterContext"]!!
918 .valueAsInstance!!
919 .run {
920 this instanceOf "android.app.Activity" &&
921 this["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true
922 }
923
ObjectReporternull924 private fun ObjectReporter.inspectContextImplOuterContext(
925 outerContext: HeapInstance,
926 contextImpl: HeapInstance,
927 prefix: String = "ContextImpl"
928 ) {
929 if (outerContext instanceOf "android.app.Activity") {
930 val mDestroyed = outerContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean
931 if (mDestroyed != null) {
932 if (mDestroyed) {
933 leakingReasons += "$prefix.mOuterContext is an instance of" +
934 " ${outerContext.instanceClassName} with Activity.mDestroyed true"
935 } else {
936 notLeakingReasons += "$prefix.mOuterContext is an instance of " +
937 "${outerContext.instanceClassName} with Activity.mDestroyed false"
938 }
939 } else {
940 labels += "$prefix.mOuterContext is an instance of ${outerContext.instanceClassName}"
941 }
942 } else if (outerContext instanceOf "android.app.Application") {
943 notLeakingReasons += "$prefix.mOuterContext is an instance of" +
944 " ${outerContext.instanceClassName} which extends android.app.Application"
945 } else if (outerContext.objectId == contextImpl.objectId) {
946 labels += "$prefix.mOuterContext == ContextImpl.this: not tied to any particular lifecycle"
947 } else {
948 labels += "$prefix.mOuterContext is an instance of ${outerContext.instanceClassName}"
949 }
950 }
951
describedWithValuenull952 private infix fun HeapField.describedWithValue(valueDescription: String): String {
953 return "${declaringClass.simpleName}#$name is $valueDescription"
954 }
955
applyFromFieldnull956 private fun ObjectReporter.applyFromField(
957 inspector: ObjectInspector,
958 field: HeapField?
959 ) {
960 if (field == null) {
961 return
962 }
963 if (field.value.isNullReference) {
964 return
965 }
966 val heapObject = field.value.asObject!!
967 val delegateReporter = ObjectReporter(heapObject)
968 inspector.inspect(delegateReporter)
969 val prefix = "${field.declaringClass.simpleName}#${field.name}:"
970
971 labels += delegateReporter.labels.map { "$prefix $it" }
972 leakingReasons += delegateReporter.leakingReasons.map { "$prefix $it" }
973 notLeakingReasons += delegateReporter.notLeakingReasons.map { "$prefix $it" }
974 }
975
976 private val HeapInstance.lifecycleRegistryState: String
977 get() {
978 // LifecycleRegistry was converted to Kotlin
979 // https://cs.android.com/androidx/platform/frameworks/support/+/36833f9ab0c50bf449fc795e297a0e124df3356e
980 val stateField = this["androidx.lifecycle.LifecycleRegistry", "state"]
981 ?: this["androidx.lifecycle.LifecycleRegistry", "mState"]!!
982 val state = stateField.valueAsInstance!!
983 return state["java.lang.Enum", "name"]!!.value.readAsJavaString()!!
984 }
985
986 /**
987 * Recursively unwraps `this` [HeapInstance] as a ContextWrapper until an Activity is found in which case it is
988 * returned. Returns null if no activity was found.
989 */
unwrapActivityContextnull990 internal fun HeapInstance.unwrapActivityContext(): HeapInstance? {
991 return unwrapComponentContext().let { context ->
992 if (context != null && context instanceOf "android.app.Activity") {
993 context
994 } else {
995 null
996 }
997 }
998 }
999
1000 /**
1001 * Recursively unwraps `this` [HeapInstance] as a ContextWrapper until an known Android component
1002 * context is found in which case it is returned. Returns null if no activity was found.
1003 */
1004 @Suppress("NestedBlockDepth", "ReturnCount")
unwrapComponentContextnull1005 internal fun HeapInstance.unwrapComponentContext(): HeapInstance? {
1006 val matchingClassName = instanceClass.classHierarchy.map { it.name }
1007 .firstOrNull {
1008 when (it) {
1009 "android.content.ContextWrapper",
1010 "android.app.Activity",
1011 "android.app.Application",
1012 "android.app.Service"
1013 -> true
1014 else -> false
1015 }
1016 }
1017 ?: return null
1018
1019 if (matchingClassName != "android.content.ContextWrapper") {
1020 return this
1021 }
1022
1023 var context = this
1024 val visitedInstances = mutableListOf<Long>()
1025 var keepUnwrapping = true
1026 while (keepUnwrapping) {
1027 visitedInstances += context.objectId
1028 keepUnwrapping = false
1029 val mBase = context["android.content.ContextWrapper", "mBase"]!!.value
1030
1031 if (mBase.isNonNullReference) {
1032 val wrapperContext = context
1033 context = mBase.asObject!!.asInstance!!
1034
1035 val contextMatchingClassName = context.instanceClass.classHierarchy.map { it.name }
1036 .firstOrNull {
1037 when (it) {
1038 "android.content.ContextWrapper",
1039 "android.app.Activity",
1040 "android.app.Application",
1041 "android.app.Service"
1042 -> true
1043 else -> false
1044 }
1045 }
1046
1047 var isContextWrapper = contextMatchingClassName == "android.content.ContextWrapper"
1048
1049 if (contextMatchingClassName == "android.app.Activity") {
1050 return context
1051 } else {
1052 if (wrapperContext instanceOf "com.android.internal.policy.DecorContext") {
1053 // mBase isn't an activity, let's unwrap DecorContext.mPhoneWindow.mContext instead
1054 val mPhoneWindowField =
1055 wrapperContext["com.android.internal.policy.DecorContext", "mPhoneWindow"]
1056 if (mPhoneWindowField != null) {
1057 val phoneWindow = mPhoneWindowField.valueAsInstance!!
1058 context = phoneWindow["android.view.Window", "mContext"]!!.valueAsInstance!!
1059 if (context instanceOf "android.app.Activity") {
1060 return context
1061 }
1062 isContextWrapper = context instanceOf "android.content.ContextWrapper"
1063 }
1064 }
1065 if (contextMatchingClassName == "android.app.Service" ||
1066 contextMatchingClassName == "android.app.Application"
1067 ) {
1068 return context
1069 }
1070 if (isContextWrapper &&
1071 // Avoids infinite loops
1072 context.objectId !in visitedInstances
1073 ) {
1074 keepUnwrapping = true
1075 }
1076 }
1077 }
1078 }
1079 return null
1080 }
1081
1082 /**
1083 * Same as [HeapInstance.readField] but throws if the field doesnt exist
1084 */
getOrThrownull1085 internal fun HeapInstance.getOrThrow(
1086 declaringClassName: String,
1087 fieldName: String
1088 ): HeapField {
1089 return this[declaringClassName, fieldName] ?: throw IllegalStateException(
1090 """
1091 $instanceClassName is expected to have a $declaringClassName.$fieldName field which cannot be found.
1092 This might be due to the app code being obfuscated. If that's the case, then the heap analysis
1093 is unable to proceed without a mapping file to deobfuscate class names.
1094 You can run LeakCanary on obfuscated builds by following the instructions at
1095 https://square.github.io/leakcanary/recipes/#using-leakcanary-with-obfuscated-apps
1096 """
1097 )
1098 }
1099