xref: /aosp_15_r20/external/leakcanary2/shark-android/src/main/java/shark/AndroidObjectInspectors.kt (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)
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