AndroidObjectInspectors.kt
TLDR
The file AndroidObjectInspectors.kt
contains a set of default ObjectInspector
implementations that are specifically designed for inspecting Android objects. These inspectors have heuristics based on common AOSP and library classes to determine if an object is potentially leaking or not. The file also includes helper functions to unwrap and inspect different types of Android contexts.
Methods
There are no methods defined in this file.
Classes
enum class AndroidObjectInspectors
This enum class implements the ObjectInspector
interface and provides default inspectors for various Android objects. Each inspector has a specific purpose and set of heuristics to determine if an object is leaking or not. Some of the inspectors provided in this enum class include:
-
VIEW
: Inspectsandroid.view.View
objects and checks if they are leaking based on their parent hierarchy and attachment status. -
EDITOR
: Inspectsandroid.widget.Editor
objects and delegates the inspection to theVIEW
inspector for the associated TextView. -
ACTIVITY
: Inspectsandroid.app.Activity
objects and checks if they are destroyed. -
SERVICE
: Inspectsandroid.app.Service
objects and checks if they are held by theActivityThread
. -
CONTEXT_FIELD
: Inspects fields of typeandroid.content.Context
in an object and provides information about the context and its lifecycle. -
CONTEXT_WRAPPER
: Inspectsandroid.content.ContextWrapper
objects and checks if they wrap an activity with a destroyed state. -
APPLICATION_PACKAGE_MANAGER
: Inspectsandroid.app.ApplicationContextManager
objects and checks if their outer context is leaking. -
CONTEXT_IMPL
: Inspectsandroid.app.ContextImpl
objects and checks if their outer context is leaking. -
DIALOG
: Inspectsandroid.app.Dialog
objects and provides information about their attachment and destruction status. -
ACTIVITY_THREAD
: Inspectsandroid.app.ActivityThread
objects and marks them as not leaking due to being singletons. -
APPLICATION
: Inspectsandroid.app.Application
objects and marks them as not leaking due to being singletons. -
INPUT_METHOD_MANAGER
: Inspectsandroid.view.inputmethod.InputMethodManager
objects and marks them as not leaking due to being singletons. -
FRAGMENT
: Inspectsandroid.app.Fragment
objects and checks if their fragment manager is null. -
SUPPORT_FRAGMENT
: Inspects support libraryandroid.support.v4.app.Fragment
objects and checks if their fragment manager is null. -
ANDROIDX_FRAGMENT
: Inspects AndroidXandroidx.fragment.app.Fragment
objects and checks if their fragment manager is null. -
MESSAGE_QUEUE
: Inspectsandroid.os.MessageQueue
objects and provides information about their quitting status. -
LOADED_APK
: Inspectsandroid.app.LoadedApk
objects and provides information about the receivers registered in them. -
MORTAR_PRESENTER
: Inspectsmortar.Presenter
objects and provides information about their view. -
MORTAR_SCOPE
: Inspectsmortar.MortarScope
objects and checks if theirdead
flag is set. -
COORDINATOR
: Inspectscom.squareup.coordinators.Coordinator
objects and provides information about their attached status. -
MAIN_THREAD
: InspectsThread
objects and marks the main thread as not leaking. -
VIEW_ROOT_IMPL
: Inspectsandroid.view.ViewRootImpl
objects and checks if their view is null or if their context references a destroyed activity. -
WINDOW
: Inspectsandroid.view.Window
objects and checks if they are marked as destroyed. -
MESSAGE
: Inspectsandroid.os.Message
objects and provides information about their fields. -
TOAST
: Inspectsandroid.widget.Toast
objects and checks if they are showing or done showing. -
RECOMPOSER
: Inspectsandroidx.compose.runtime.Recomposer
objects and provides information about their state. -
COMPOSITION_IMPL
: Inspectsandroidx.compose.runtime.CompositionImpl
objects and checks if they are disposed. -
ANIMATOR
: Inspectsandroid.animation.Animator
objects and provides information about their listener list. -
OBJECT_ANIMATOR
: Inspectsandroid.animation.ObjectAnimator
objects and provides detailed information about their properties and animation state. -
LIFECYCLE_REGISTRY
: Inspectsandroidx.lifecycle.LifecycleRegistry
objects and checks their state.
companion object AndroidObjectInspectors
This companion object provides additional functions and properties related to the AndroidObjectInspectors
enum class. It includes a property appDefaults
that returns a list of default ObjectInspector
implementations suitable for apps. It also includes a function createLeakingObjectFilters
that creates a list of LeakingObjectFilter
based on the passed set of AndroidObjectInspectors
.
Extension functions and properties
This file includes several extension functions and properties for the HeapInstance
class, such as unwrapActivityContext()
and unwrapComponentContext()
, which are used to recursively unwrap and inspect different types of Android contexts.
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package shark
import shark.AndroidObjectInspectors.Companion.appDefaults
import shark.AndroidServices.aliveAndroidServiceObjectIds
import shark.FilteringLeakingObjectFinder.LeakingObjectFilter
import shark.HeapObject.HeapInstance
import java.util.EnumSet
import kotlin.math.absoluteValue
import shark.internal.InternalSharkCollectionsHelper
/**
* A set of default [ObjectInspector]s that knows about common AOSP and library
* classes.
*
* These are heuristics based on our experience and knowledge of AOSP and various library
* internals. We only make a decision if we're reasonably sure the state of an object is
* unlikely to be the result of a programmer mistake.
*
* For example, no matter how many mistakes we make in our code, the value of Activity.mDestroy
* will not be influenced by those mistakes.
*
* Most developers should use the entire set of default [ObjectInspector] by calling [appDefaults],
* unless there's a bug and you temporarily want to remove an inspector.
*/
enum class AndroidObjectInspectors : ObjectInspector {
VIEW {
override val leakingObjectFilter = { heapObject: HeapObject ->
if (heapObject is HeapInstance && heapObject instanceOf "android.view.View") {
// Leaking if null parent or non view parent.
val viewParent = heapObject["android.view.View", "mParent"]!!.valueAsInstance
val isParentlessView = viewParent == null
val isChildOfViewRootImpl =
viewParent != null && !(viewParent instanceOf "android.view.View")
val isRootView = isParentlessView || isChildOfViewRootImpl
// This filter only cares for root view because we only need one view in a view hierarchy.
if (isRootView) {
val mContext = heapObject["android.view.View", "mContext"]!!.value.asObject!!.asInstance!!
val activityContext = mContext.unwrapActivityContext()
val mContextIsDestroyedActivity = (activityContext != null &&
activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true)
if (mContextIsDestroyedActivity) {
// Root view with unwrapped mContext a destroyed activity.
true
} else {
val viewDetached =
heapObject["android.view.View", "mAttachInfo"]!!.value.isNullReference
if (viewDetached) {
val mWindowAttachCount =
heapObject["android.view.View", "mWindowAttachCount"]?.value!!.asInt!!
if (mWindowAttachCount > 0) {
when {
isChildOfViewRootImpl -> {
// Child of ViewRootImpl that was once attached and is now detached.
// Unwrapped mContext not a destroyed activity. This could be a dialog root.
true
}
heapObject.instanceClassName == "com.android.internal.policy.DecorView" -> {
// DecorView with null parent, once attached now detached.
// Unwrapped mContext not a destroyed activity. This could be a dialog root.
// Unlikely to be a reusable cached view => leak.
true
}
else -> {
// View with null parent, once attached now detached.
// Unwrapped mContext not a destroyed activity. This could be a dialog root.
// Could be a leak or could be a reusable cached view.
false
}
}
} else {
// Root view, detached but was never attached.
// This could be a cached instance.
false
}
} else {
// Root view that is attached.
false
}
}
} else {
// Not a root view.
false
}
} else {
// Not a view
false
}
}
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.view.View") { instance ->
// This skips edge cases like Toast$TN.mNextView holding on to an unattached and unparented
// next toast view
var rootParent = instance["android.view.View", "mParent"]!!.valueAsInstance
var rootView: HeapInstance? = null
while (rootParent != null && rootParent instanceOf "android.view.View") {
rootView = rootParent
rootParent = rootParent["android.view.View", "mParent"]!!.valueAsInstance
}
val partOfWindowHierarchy = rootParent != null || (rootView != null &&
rootView.instanceClassName == "com.android.internal.policy.DecorView")
val mWindowAttachCount =
instance["android.view.View", "mWindowAttachCount"]?.value!!.asInt!!
val viewDetached = instance["android.view.View", "mAttachInfo"]!!.value.isNullReference
val mContext = instance["android.view.View", "mContext"]!!.value.asObject!!.asInstance!!
val activityContext = mContext.unwrapActivityContext()
if (activityContext != null && activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true) {
leakingReasons += "View.mContext references a destroyed activity"
} else {
if (partOfWindowHierarchy && mWindowAttachCount > 0) {
if (viewDetached) {
leakingReasons += "View detached yet still part of window view hierarchy"
} else {
if (rootView != null && rootView["android.view.View", "mAttachInfo"]!!.value.isNullReference) {
leakingReasons += "View attached but root view ${rootView.instanceClassName} detached (attach disorder)"
} else {
notLeakingReasons += "View attached"
}
}
}
}
labels += if (partOfWindowHierarchy) {
"View is part of a window view hierarchy"
} else {
"View not part of a window view hierarchy"
}
labels += if (viewDetached) {
"View.mAttachInfo is null (view detached)"
} else {
"View.mAttachInfo is not null (view attached)"
}
AndroidResourceIdNames.readFromHeap(instance.graph)
?.let { resIds ->
val mID = instance["android.view.View", "mID"]!!.value.asInt!!
val noViewId = -1
if (mID != noViewId) {
val resourceName = resIds[mID]
labels += "View.mID = R.id.$resourceName"
}
}
labels += "View.mWindowAttachCount = $mWindowAttachCount"
}
}
},
EDITOR {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf "android.widget.Editor" &&
heapObject["android.widget.Editor", "mTextView"]?.value?.asObject?.let { textView ->
VIEW.leakingObjectFilter!!(textView)
} ?: false
}
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("android.widget.Editor") { instance ->
applyFromField(VIEW, instance["android.widget.Editor", "mTextView"])
}
}
},
ACTIVITY {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf "android.app.Activity" &&
heapObject["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true
}
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.app.Activity") { instance ->
// Activity.mDestroyed was introduced in 17.
// https://android.googlesource.com/platform/frameworks/base/+
// /6d9dcbccec126d9b87ab6587e686e28b87e5a04d
val field = instance["android.app.Activity", "mDestroyed"]
if (field != null) {
if (field.value.asBoolean!!) {
leakingReasons += field describedWithValue "true"
} else {
notLeakingReasons += field describedWithValue "false"
}
}
}
}
},
SERVICE {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf "android.app.Service" &&
heapObject.objectId !in heapObject.graph.aliveAndroidServiceObjectIds
}
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.app.Service") { instance ->
if (instance.objectId in instance.graph.aliveAndroidServiceObjectIds) {
notLeakingReasons += "Service held by ActivityThread"
} else {
leakingReasons += "Service not held by ActivityThread"
}
}
}
},
CONTEXT_FIELD {
override fun inspect(reporter: ObjectReporter) {
val instance = reporter.heapObject
if (instance !is HeapInstance) {
return
}
instance.readFields().forEach { field ->
val fieldInstance = field.valueAsInstance
if (fieldInstance != null && fieldInstance instanceOf "android.content.Context") {
reporter.run {
val componentContext = fieldInstance.unwrapComponentContext()
labels += if (componentContext == null) {
"${field.name} instance of ${fieldInstance.instanceClassName}"
} else if (componentContext instanceOf "android.app.Activity") {
val activityDescription =
"with mDestroyed = " + (componentContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean?.toString()
?: "UNKNOWN")
if (componentContext == fieldInstance) {
"${field.name} instance of ${fieldInstance.instanceClassName} $activityDescription"
} else {
"${field.name} instance of ${fieldInstance.instanceClassName}, " +
"wrapping activity ${componentContext.instanceClassName} $activityDescription"
}
} else if (componentContext == fieldInstance) {
// No need to add "instance of Application / Service", devs know their own classes.
"${field.name} instance of ${fieldInstance.instanceClassName}"
} else {
"${field.name} instance of ${fieldInstance.instanceClassName}, wrapping ${componentContext.instanceClassName}"
}
}
}
}
}
},
CONTEXT_WRAPPER {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject.unwrapActivityContext()
?.get("android.app.Activity", "mDestroyed")?.value?.asBoolean == true
}
override fun inspect(
reporter: ObjectReporter
) {
val instance = reporter.heapObject
if (instance !is HeapInstance) {
return
}
// We're looking for ContextWrapper instances that are not Activity, Application or Service.
// So we stop whenever we find any of those 4 classes, and then only keep ContextWrapper.
val matchingClassName = instance.instanceClass.classHierarchy.map { it.name }
.firstOrNull {
when (it) {
"android.content.ContextWrapper",
"android.app.Activity",
"android.app.Application",
"android.app.Service"
-> true
else -> false
}
}
if (matchingClassName == "android.content.ContextWrapper") {
reporter.run {
val componentContext = instance.unwrapComponentContext()
if (componentContext != null) {
if (componentContext instanceOf "android.app.Activity") {
val mDestroyed = componentContext["android.app.Activity", "mDestroyed"]
if (mDestroyed != null) {
if (mDestroyed.value.asBoolean!!) {
leakingReasons += "${instance.instanceClassSimpleName} wraps an Activity with Activity.mDestroyed true"
} else {
// We can't assume it's not leaking, because this context might have a shorter lifecycle
// than the activity. So we'll just add a label.
labels += "${instance.instanceClassSimpleName} wraps an Activity with Activity.mDestroyed false"
}
}
} else if (componentContext instanceOf "android.app.Application") {
labels += "${instance.instanceClassSimpleName} wraps an Application context"
} else {
labels += "${instance.instanceClassSimpleName} wraps a Service context"
}
} else {
labels += "${instance.instanceClassSimpleName} does not wrap a known Android context"
}
}
}
}
},
APPLICATION_PACKAGE_MANAGER {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf "android.app.ApplicationContextManager" &&
heapObject["android.app.ApplicationContextManager", "mContext"]!!
.valueAsInstance!!.outerContextIsLeaking()
}
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("android.app.ApplicationContextManager") { instance ->
val outerContext = instance["android.app.ApplicationContextManager", "mContext"]!!
.valueAsInstance!!["android.app.ContextImpl", "mOuterContext"]!!
.valueAsInstance!!
inspectContextImplOuterContext(outerContext, instance, "ApplicationContextManager.mContext")
}
}
},
CONTEXT_IMPL {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf "android.app.ContextImpl" &&
heapObject.outerContextIsLeaking()
}
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("android.app.ContextImpl") { instance ->
val outerContext = instance["android.app.ContextImpl", "mOuterContext"]!!
.valueAsInstance!!
inspectContextImplOuterContext(outerContext, instance)
}
}
},
DIALOG {
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.app.Dialog") { instance ->
val mDecor = instance["android.app.Dialog", "mDecor"]!!
// Can't infer leaking status: mDecor null means either never shown or dismiss.
// mDecor non null means the dialog is showing, but sometimes dialogs stay showing
// after activity destroyed so that's not really a non leak either.
labels += mDecor describedWithValue if (mDecor.value.isNullReference) {
"null"
} else {
"not null"
}
}
}
},
ACTIVITY_THREAD {
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("android.app.ActivityThread") {
notLeakingReasons += "ActivityThread is a singleton"
}
}
},
APPLICATION {
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.app.Application") {
notLeakingReasons += "Application is a singleton"
}
}
},
INPUT_METHOD_MANAGER {
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.view.inputmethod.InputMethodManager") {
notLeakingReasons += "InputMethodManager is a singleton"
}
}
},
FRAGMENT {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf "android.app.Fragment" &&
heapObject["android.app.Fragment", "mFragmentManager"]!!.value.isNullReference
}
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.app.Fragment") { instance ->
val fragmentManager = instance["android.app.Fragment", "mFragmentManager"]!!
if (fragmentManager.value.isNullReference) {
leakingReasons += fragmentManager describedWithValue "null"
} else {
notLeakingReasons += fragmentManager describedWithValue "not null"
}
val mTag = instance["android.app.Fragment", "mTag"]?.value?.readAsJavaString()
if (!mTag.isNullOrEmpty()) {
labels += "Fragment.mTag=$mTag"
}
}
}
},
SUPPORT_FRAGMENT {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf ANDROID_SUPPORT_FRAGMENT_CLASS_NAME &&
heapObject.getOrThrow(
ANDROID_SUPPORT_FRAGMENT_CLASS_NAME, "mFragmentManager"
).value.isNullReference
}
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf(ANDROID_SUPPORT_FRAGMENT_CLASS_NAME) { instance ->
val fragmentManager =
instance.getOrThrow(ANDROID_SUPPORT_FRAGMENT_CLASS_NAME, "mFragmentManager")
if (fragmentManager.value.isNullReference) {
leakingReasons += fragmentManager describedWithValue "null"
} else {
notLeakingReasons += fragmentManager describedWithValue "not null"
}
val mTag = instance[ANDROID_SUPPORT_FRAGMENT_CLASS_NAME, "mTag"]?.value?.readAsJavaString()
if (!mTag.isNullOrEmpty()) {
labels += "Fragment.mTag=$mTag"
}
}
}
},
ANDROIDX_FRAGMENT {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf "androidx.fragment.app.Fragment" &&
heapObject.getOrThrow(
"androidx.fragment.app.Fragment", "mFragmentManager"
).value.isNullReference
}
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("androidx.fragment.app.Fragment") { instance ->
val fragmentManager =
instance.getOrThrow("androidx.fragment.app.Fragment", "mFragmentManager")
if (fragmentManager.value.isNullReference) {
leakingReasons += fragmentManager describedWithValue "null"
} else {
notLeakingReasons += fragmentManager describedWithValue "not null"
}
val mTag = instance["androidx.fragment.app.Fragment", "mTag"]?.value?.readAsJavaString()
if (!mTag.isNullOrEmpty()) {
labels += "Fragment.mTag=$mTag"
}
}
}
},
MESSAGE_QUEUE {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf "android.os.MessageQueue" &&
(heapObject["android.os.MessageQueue", "mQuitting"]
?: heapObject["android.os.MessageQueue", "mQuiting"]!!).value.asBoolean!!
}
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.os.MessageQueue") { instance ->
// mQuiting had a typo and was renamed to mQuitting
// https://android.googlesource.com/platform/frameworks/base/+/013cf847bcfd2828d34dced60adf2d3dd98021dc
val mQuitting = instance["android.os.MessageQueue", "mQuitting"]
?: instance["android.os.MessageQueue", "mQuiting"]!!
if (mQuitting.value.asBoolean!!) {
leakingReasons += mQuitting describedWithValue "true"
} else {
notLeakingReasons += mQuitting describedWithValue "false"
}
val queueHead = instance["android.os.MessageQueue", "mMessages"]!!.valueAsInstance
if (queueHead != null) {
val targetHandler = queueHead["android.os.Message", "target"]!!.valueAsInstance
if (targetHandler != null) {
val looper = targetHandler["android.os.Handler", "mLooper"]!!.valueAsInstance
if (looper != null) {
val thread = looper["android.os.Looper", "mThread"]!!.valueAsInstance!!
val threadName = thread[Thread::class, "name"]!!.value.readAsJavaString()
labels += "HandlerThread: \"$threadName\""
}
}
}
}
}
},
LOADED_APK {
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.app.LoadedApk") { instance ->
val receiversMap = instance["android.app.LoadedApk", "mReceivers"]!!.valueAsInstance!!
val receiversArray = receiversMap["android.util.ArrayMap", "mArray"]!!.valueAsObjectArray!!
val receiverContextList = receiversArray.readElements().toList()
val allReceivers = (receiverContextList.indices step 2).mapNotNull { index ->
val context = receiverContextList[index]
if (context.isNonNullReference) {
val contextReceiversMap = receiverContextList[index + 1].asObject!!.asInstance!!
val contextReceivers = contextReceiversMap["android.util.ArrayMap", "mArray"]!!
.valueAsObjectArray!!
.readElements()
.toList()
val receivers =
(contextReceivers.indices step 2).mapNotNull { contextReceivers[it].asObject?.asInstance }
val contextInstance = context.asObject!!.asInstance!!
val contextString =
"${contextInstance.instanceClassSimpleName}@${contextInstance.objectId}"
contextString to receivers.map { "${it.instanceClassSimpleName}@${it.objectId}" }
} else {
null
}
}.toList()
if (allReceivers.isNotEmpty()) {
labels += "Receivers"
allReceivers.forEach { (contextString, receiverStrings) ->
labels += "..$contextString"
receiverStrings.forEach { receiverString ->
labels += "....$receiverString"
}
}
}
}
}
},
MORTAR_PRESENTER {
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("mortar.Presenter") { instance ->
val view = instance.getOrThrow("mortar.Presenter", "view")
labels += view describedWithValue if (view.value.isNullReference) "null" else "not null"
}
}
},
MORTAR_SCOPE {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf "mortar.MortarScope" &&
heapObject.getOrThrow("mortar.MortarScope", "dead").value.asBoolean!!
}
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("mortar.MortarScope") { instance ->
val dead = instance.getOrThrow("mortar.MortarScope", "dead").value.asBoolean!!
val scopeName = instance.getOrThrow("mortar.MortarScope", "name").value.readAsJavaString()
if (dead) {
leakingReasons += "mortar.MortarScope.dead is true for scope $scopeName"
} else {
notLeakingReasons += "mortar.MortarScope.dead is false for scope $scopeName"
}
}
}
},
COORDINATOR {
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("com.squareup.coordinators.Coordinator") { instance ->
val attached = instance.getOrThrow("com.squareup.coordinators.Coordinator", "attached")
labels += attached describedWithValue "${attached.value.asBoolean}"
}
}
},
MAIN_THREAD {
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf(Thread::class) { instance ->
val threadName = instance[Thread::class, "name"]!!.value.readAsJavaString()
if (threadName == "main") {
notLeakingReasons += "the main thread always runs"
}
}
}
},
VIEW_ROOT_IMPL {
override val leakingObjectFilter = { heapObject: HeapObject ->
if (heapObject is HeapInstance &&
heapObject instanceOf "android.view.ViewRootImpl"
) {
if (heapObject["android.view.ViewRootImpl", "mView"]!!.value.isNullReference) {
true
} else {
val mContextField = heapObject["android.view.ViewRootImpl", "mContext"]
if (mContextField != null) {
val mContext = mContextField.valueAsInstance!!
val activityContext = mContext.unwrapActivityContext()
(activityContext != null && activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true)
} else {
false
}
}
} else {
false
}
}
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("android.view.ViewRootImpl") { instance ->
val mViewField = instance["android.view.ViewRootImpl", "mView"]!!
if (mViewField.value.isNullReference) {
leakingReasons += mViewField describedWithValue "null"
} else {
// ViewRootImpl.mContext wasn't always here.
val mContextField = instance["android.view.ViewRootImpl", "mContext"]
if (mContextField != null) {
val mContext = mContextField.valueAsInstance!!
val activityContext = mContext.unwrapActivityContext()
if (activityContext != null && activityContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true) {
leakingReasons += "ViewRootImpl.mContext references a destroyed activity, did you forget to cancel toasts or dismiss dialogs?"
}
}
labels += mViewField describedWithValue "not null"
}
val mWindowAttributes =
instance["android.view.ViewRootImpl", "mWindowAttributes"]!!.valueAsInstance!!
val mTitleField = mWindowAttributes["android.view.WindowManager\$LayoutParams", "mTitle"]!!
labels += if (mTitleField.value.isNonNullReference) {
val mTitle =
mTitleField.valueAsInstance!!.readAsJavaString()!!
"mWindowAttributes.mTitle = \"$mTitle\""
} else {
"mWindowAttributes.mTitle is null"
}
val type =
mWindowAttributes["android.view.WindowManager\$LayoutParams", "type"]!!.value.asInt!!
// android.view.WindowManager.LayoutParams.TYPE_TOAST
val details = if (type == 2005) {
" (Toast)"
} else ""
labels += "mWindowAttributes.type = $type$details"
}
}
},
WINDOW {
override val leakingObjectFilter = { heapObject: HeapObject ->
heapObject is HeapInstance &&
heapObject instanceOf "android.view.Window" &&
heapObject["android.view.Window", "mDestroyed"]!!.value.asBoolean!!
}
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.view.Window") { instance ->
val mDestroyed = instance["android.view.Window", "mDestroyed"]!!
if (mDestroyed.value.asBoolean!!) {
leakingReasons += mDestroyed describedWithValue "true"
} else {
// A dialog window could be leaking, destroy is only set to false for activity windows.
labels += mDestroyed describedWithValue "false"
}
}
}
},
MESSAGE {
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("android.os.Message") { instance ->
labels += "Message.what = ${instance["android.os.Message", "what"]!!.value.asInt}"
val heapDumpUptimeMillis = KeyedWeakReferenceFinder.heapDumpUptimeMillis(instance.graph)
val whenUptimeMillis = instance["android.os.Message", "when"]!!.value.asLong!!
labels += if (heapDumpUptimeMillis != null) {
val diffMs = whenUptimeMillis - heapDumpUptimeMillis
if (diffMs > 0) {
"Message.when = $whenUptimeMillis ($diffMs ms after heap dump)"
} else {
"Message.when = $whenUptimeMillis (${diffMs.absoluteValue} ms before heap dump)"
}
} else {
"Message.when = $whenUptimeMillis"
}
labels += "Message.obj = ${instance["android.os.Message", "obj"]!!.value.asObject}"
labels += "Message.callback = ${instance["android.os.Message", "callback"]!!.value.asObject}"
labels += "Message.target = ${instance["android.os.Message", "target"]!!.value.asObject}"
}
}
},
TOAST {
override val leakingObjectFilter = { heapObject: HeapObject ->
if (heapObject is HeapInstance && heapObject instanceOf "android.widget.Toast") {
val tnInstance =
heapObject["android.widget.Toast", "mTN"]!!.value.asObject!!.asInstance!!
(tnInstance["android.widget.Toast\$TN", "mWM"]!!.value.isNonNullReference &&
tnInstance["android.widget.Toast\$TN", "mView"]!!.value.isNullReference)
} else false
}
override fun inspect(
reporter: ObjectReporter
) {
reporter.whenInstanceOf("android.widget.Toast") { instance ->
val tnInstance =
instance["android.widget.Toast", "mTN"]!!.value.asObject!!.asInstance!!
// mWM is set in android.widget.Toast.TN#handleShow and never unset, so this toast was never
// shown, we don't know if it's leaking.
if (tnInstance["android.widget.Toast\$TN", "mWM"]!!.value.isNonNullReference) {
// mView is reset to null in android.widget.Toast.TN#handleHide
if (tnInstance["android.widget.Toast\$TN", "mView"]!!.value.isNullReference) {
leakingReasons += "This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null)"
} else {
notLeakingReasons += "This toast is showing (Toast.mTN.mWM != null && Toast.mTN.mView != null)"
}
}
}
}
},
RECOMPOSER {
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("androidx.compose.runtime.Recomposer") { instance ->
val stateFlow =
instance["androidx.compose.runtime.Recomposer", "_state"]!!.valueAsInstance!!
val state = stateFlow["kotlinx.coroutines.flow.StateFlowImpl", "_state"]?.valueAsInstance
if (state != null) {
val stateName = state["java.lang.Enum", "name"]!!.valueAsInstance!!.readAsJavaString()!!
val label = "Recomposer is in state $stateName"
when (stateName) {
"ShutDown", "ShuttingDown" -> leakingReasons += label
"Inactive", "InactivePendingWork" -> labels += label
"PendingWork", "Idle" -> notLeakingReasons += label
}
}
}
}
},
COMPOSITION_IMPL {
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("androidx.compose.runtime.CompositionImpl") { instance ->
if (instance["androidx.compose.runtime.CompositionImpl", "disposed"]!!.value.asBoolean!!) {
leakingReasons += "Composition disposed"
} else {
notLeakingReasons += "Composition not disposed"
}
}
}
},
ANIMATOR {
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("android.animation.Animator") { instance ->
val mListeners = instance["android.animation.Animator", "mListeners"]!!.valueAsInstance
if (mListeners != null) {
val listenerValues = InternalSharkCollectionsHelper.arrayListValues(mListeners).toList()
if (listenerValues.isNotEmpty()) {
listenerValues.forEach { value ->
labels += "mListeners$value"
}
} else {
labels += "mListeners is empty"
}
} else {
labels += "mListeners = null"
}
}
}
},
OBJECT_ANIMATOR {
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("android.animation.ObjectAnimator") { instance ->
labels += "mPropertyName = " + (instance["android.animation.ObjectAnimator", "mPropertyName"]!!.valueAsInstance?.readAsJavaString()
?: "null")
val mProperty = instance["android.animation.ObjectAnimator", "mProperty"]!!.valueAsInstance
if (mProperty == null) {
labels += "mProperty = null"
} else {
labels += "mProperty.mName = " + (mProperty["android.util.Property", "mName"]!!.valueAsInstance?.readAsJavaString()
?: "null")
labels += "mProperty.mType = " + (mProperty["android.util.Property", "mType"]!!.valueAsClass?.name
?: "null")
}
labels += "mInitialized = " + instance["android.animation.ValueAnimator", "mInitialized"]!!.value.asBoolean!!
labels += "mStarted = " + instance["android.animation.ValueAnimator", "mStarted"]!!.value.asBoolean!!
labels += "mRunning = " + instance["android.animation.ValueAnimator", "mRunning"]!!.value.asBoolean!!
labels += "mAnimationEndRequested = " + instance["android.animation.ValueAnimator", "mAnimationEndRequested"]!!.value.asBoolean!!
labels += "mDuration = " + instance["android.animation.ValueAnimator", "mDuration"]!!.value.asLong!!
labels += "mStartDelay = " + instance["android.animation.ValueAnimator", "mStartDelay"]!!.value.asLong!!
val repeatCount = instance["android.animation.ValueAnimator", "mRepeatCount"]!!.value.asInt!!
labels += "mRepeatCount = " + if (repeatCount == -1) "INFINITE (-1)" else repeatCount
val repeatModeConstant = when (val repeatMode =
instance["android.animation.ValueAnimator", "mRepeatMode"]!!.value.asInt!!) {
1 -> "RESTART (1)"
2 -> "REVERSE (2)"
else -> "Unknown ($repeatMode)"
}
labels += "mRepeatMode = $repeatModeConstant"
}
}
},
LIFECYCLE_REGISTRY {
override fun inspect(reporter: ObjectReporter) {
reporter.whenInstanceOf("androidx.lifecycle.LifecycleRegistry") { instance ->
val state = instance.lifecycleRegistryState
labels += "mState = $state"
// If state is DESTROYED, this doesn't mean the LifecycleRegistry itself is leaking.
// Fragment.mViewLifecycleRegistry becomes DESTROYED when the fragment view is destroyed,
// but the registry itself is still held in memory by the fragment.
if (state != "DESTROYED") {
notLeakingReasons += "mState is not DESTROYED"
}
}
}
private val HeapInstance.lifecycleRegistryState: String
get() {
// LifecycleRegistry was converted to Kotlin
// https://cs.android.com/androidx/platform/frameworks/support/+/36833f9ab0c50bf449fc795e297a0e124df3356e
val stateField = this["androidx.lifecycle.LifecycleRegistry", "state"]
?: this["androidx.lifecycle.LifecycleRegistry", "mState"]!!
val state = stateField.valueAsInstance!!
return state["java.lang.Enum", "name"]!!.value.readAsJavaString()!!
}
},
;
internal open val leakingObjectFilter: ((heapObject: HeapObject) -> Boolean)? = null
companion object {
/** @see AndroidObjectInspectors */
val appDefaults: List<ObjectInspector>
get() = ObjectInspectors.jdkDefaults + values()
/**
* Returns a list of [LeakingObjectFilter] suitable for apps.
*/
val appLeakingObjectFilters: List<LeakingObjectFilter> =
ObjectInspectors.jdkLeakingObjectFilters +
createLeakingObjectFilters(EnumSet.allOf(AndroidObjectInspectors::class.java))
/**
* Creates a list of [LeakingObjectFilter] based on the passed in [AndroidObjectInspectors].
*/
fun createLeakingObjectFilters(inspectors: Set<AndroidObjectInspectors>): List<LeakingObjectFilter> =
inspectors.mapNotNull { it.leakingObjectFilter }
.map { filter ->
LeakingObjectFilter { heapObject -> filter(heapObject) }
}
}
// Using a string builder to prevent Jetifier from changing this string to Android X Fragment
@Suppress("VariableNaming", "PropertyName")
internal val ANDROID_SUPPORT_FRAGMENT_CLASS_NAME =
StringBuilder("android.").append("support.v4.app.Fragment")
.toString()
}
private fun HeapInstance.outerContextIsLeaking() =
this["android.app.ContextImpl", "mOuterContext"]!!
.valueAsInstance!!
.run {
this instanceOf "android.app.Activity" &&
this["android.app.Activity", "mDestroyed"]?.value?.asBoolean == true
}
private fun ObjectReporter.inspectContextImplOuterContext(
outerContext: HeapInstance,
contextImpl: HeapInstance,
prefix: String = "ContextImpl"
) {
if (outerContext instanceOf "android.app.Activity") {
val mDestroyed = outerContext["android.app.Activity", "mDestroyed"]?.value?.asBoolean
if (mDestroyed != null) {
if (mDestroyed) {
leakingReasons += "$prefix.mOuterContext is an instance of" +
" ${outerContext.instanceClassName} with Activity.mDestroyed true"
} else {
notLeakingReasons += "$prefix.mOuterContext is an instance of " +
"${outerContext.instanceClassName} with Activity.mDestroyed false"
}
} else {
labels += "$prefix.mOuterContext is an instance of ${outerContext.instanceClassName}"
}
} else if (outerContext instanceOf "android.app.Application") {
notLeakingReasons += "$prefix.mOuterContext is an instance of" +
" ${outerContext.instanceClassName} which extends android.app.Application"
} else if (outerContext.objectId == contextImpl.objectId) {
labels += "$prefix.mOuterContext == ContextImpl.this: not tied to any particular lifecycle"
} else {
labels += "$prefix.mOuterContext is an instance of ${outerContext.instanceClassName}"
}
}
private infix fun HeapField.describedWithValue(valueDescription: String): String {
return "${declaringClass.simpleName}#$name is $valueDescription"
}
private fun ObjectReporter.applyFromField(
inspector: ObjectInspector,
field: HeapField?
) {
if (field == null) {
return
}
if (field.value.isNullReference) {
return
}
val heapObject = field.value.asObject!!
val delegateReporter = ObjectReporter(heapObject)
inspector.inspect(delegateReporter)
val prefix = "${field.declaringClass.simpleName}#${field.name}:"
labels += delegateReporter.labels.map { "$prefix $it" }
leakingReasons += delegateReporter.leakingReasons.map { "$prefix $it" }
notLeakingReasons += delegateReporter.notLeakingReasons.map { "$prefix $it" }
}
/**
* Recursively unwraps `this` [HeapInstance] as a ContextWrapper until an Activity is found in which case it is
* returned. Returns null if no activity was found.
*/
internal fun HeapInstance.unwrapActivityContext(): HeapInstance? {
return unwrapComponentContext().let { context ->
if (context != null && context instanceOf "android.app.Activity") {
context
} else {
null
}
}
}
/**
* Recursively unwraps `this` [HeapInstance] as a ContextWrapper until an known Android component
* context is found in which case it is returned. Returns null if no activity was found.
*/
@Suppress("NestedBlockDepth", "ReturnCount")
internal fun HeapInstance.unwrapComponentContext(): HeapInstance? {
val matchingClassName = instanceClass.classHierarchy.map { it.name }
.firstOrNull {
when (it) {
"android.content.ContextWrapper",
"android.app.Activity",
"android.app.Application",
"android.app.Service"
-> true
else -> false
}
}
?: return null
if (matchingClassName != "android.content.ContextWrapper") {
return this
}
var context = this
val visitedInstances = mutableListOf<Long>()
var keepUnwrapping = true
while (keepUnwrapping) {
visitedInstances += context.objectId
keepUnwrapping = false
val mBase = context["android.content.ContextWrapper", "mBase"]!!.value
if (mBase.isNonNullReference) {
val wrapperContext = context
context = mBase.asObject!!.asInstance!!
val contextMatchingClassName = context.instanceClass.classHierarchy.map { it.name }
.firstOrNull {
when (it) {
"android.content.ContextWrapper",
"android.app.Activity",
"android.app.Application",
"android.app.Service"
-> true
else -> false
}
}
var isContextWrapper = contextMatchingClassName == "android.content.ContextWrapper"
if (contextMatchingClassName == "android.app.Activity") {
return context
} else {
if (wrapperContext instanceOf "com.android.internal.policy.DecorContext") {
// mBase isn't an activity, let's unwrap DecorContext.mPhoneWindow.mContext instead
val mPhoneWindowField =
wrapperContext["com.android.internal.policy.DecorContext", "mPhoneWindow"]
if (mPhoneWindowField != null) {
val phoneWindow = mPhoneWindowField.valueAsInstance!!
context = phoneWindow["android.view.Window", "mContext"]!!.valueAsInstance!!
if (context instanceOf "android.app.Activity") {
return context
}
isContextWrapper = context instanceOf "android.content.ContextWrapper"
}
}
if (contextMatchingClassName == "android.app.Service" ||
contextMatchingClassName == "android.app.Application"
) {
return context
}
if (isContextWrapper &&
// Avoids infinite loops
context.objectId !in visitedInstances
) {
keepUnwrapping = true
}
}
}
}
return null
}
/**
* Same as [HeapInstance.readField] but throws if the field doesnt exist
*/
internal fun HeapInstance.getOrThrow(
declaringClassName: String,
fieldName: String
): HeapField {
return this[declaringClassName, fieldName] ?: throw IllegalStateException(
"""
$instanceClassName is expected to have a $declaringClassName.$fieldName field which cannot be found.
This might be due to the app code being obfuscated. If that's the case, then the heap analysis
is unable to proceed without a mapping file to deobfuscate class names.
You can run LeakCanary on obfuscated builds by following the instructions at
https://square.github.io/leakcanary/recipes/#using-leakcanary-with-obfuscated-apps
"""
)
}