/* * 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 java.util.EnumSet import kotlin.math.absoluteValue import shark.AndroidObjectInspectors.Companion.appDefaults import shark.AndroidServices.aliveAndroidServiceObjectIds import shark.FilteringLeakingObjectFinder.LeakingObjectFilter import shark.HeapObject.HeapInstance 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["androidx.fragment.app.Fragment", "mLifecycleRegistry"]!! .valueAsInstance ?.lifecycleRegistryState == "DESTROYED" } override fun inspect( reporter: ObjectReporter ) { reporter.whenInstanceOf("androidx.fragment.app.Fragment") { instance -> val lifecycleRegistryField = instance["androidx.fragment.app.Fragment", "mLifecycleRegistry"]!! val lifecycleRegistry = lifecycleRegistryField.valueAsInstance if (lifecycleRegistry != null) { val state = lifecycleRegistry.lifecycleRegistryState val reason = "Fragment.mLifecycleRegistry.state is $state" if (state == "DESTROYED") { leakingReasons += reason } else { notLeakingReasons += reason } } else { labels += "Fragment.mLifecycleRegistry = 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 // 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 += "state is $state" } else { labels += "state = $state" } } } }, STUB { override fun inspect(reporter: ObjectReporter) { reporter.whenInstanceOf("android.os.Binder") { instance -> labels + "${instance.instanceClassSimpleName} is a binder stub. Binder stubs will often be" + " retained long after the associated activity or service is destroyed, as by design stubs" + " are retained until the other side gets GCed. If ${instance.instanceClassSimpleName} is" + " not a *static* inner class then that's most likely the root cause of this leak. Make" + " it static. If ${instance.instanceClassSimpleName} is an Android Framework class, file" + " a ticket here: https://issuetracker.google.com/issues/new?component=192705" } } }, ; internal open val leakingObjectFilter: ((heapObject: HeapObject) -> Boolean)? = null companion object { /** @see AndroidObjectInspectors */ val appDefaults: List get() = ObjectInspectors.jdkDefaults + values() /** * Returns a list of [LeakingObjectFilter] suitable for apps. */ val appLeakingObjectFilters: List = ObjectInspectors.jdkLeakingObjectFilters + createLeakingObjectFilters(EnumSet.allOf(AndroidObjectInspectors::class.java)) /** * Creates a list of [LeakingObjectFilter] based on the passed in [AndroidObjectInspectors]. */ fun createLeakingObjectFilters(inspectors: Set): List = 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" } } 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()!! } /** * 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() 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 """ ) }