main

square/leakcanary

Last updated at: 29/12/2023 09:39

AndroidReferenceReaders.kt

TLDR

This file defines an enum class called AndroidReferenceReaders. It provides several optional factory methods that create VirtualInstanceReferenceReader instances. These reader instances are used to traverse and analyze references in an Android heap graph.

Methods

ACTIVITY_THREAD__NEW_ACTIVITIES

This method creates a VirtualInstanceReferenceReader instance that reads references in the mNewActivities field of an ActivityThread instance. This method is used to identify and analyze leaks in the mNewActivities field.

MESSAGE_QUEUE

This method creates a VirtualInstanceReferenceReader instance that reads references in the mMessages field of a MessageQueue instance. This method is used to traverse the messages in a message queue.

ANIMATOR_WEAK_REF_SUCKS

This method creates a VirtualInstanceReferenceReader instance that reads references in the mTarget field of an ObjectAnimator instance. This method is used to detect weak references that may lead to leaks.

SAFE_ITERABLE_MAP

This method creates a VirtualInstanceReferenceReader instance that reads key-value pairs in a SafeIterableMap instance. This method is used to iterate over a map and analyze references in its entries.

ARRAY_SET

This method creates a VirtualInstanceReferenceReader instance that reads elements in an ArraySet instance. This method is used to traverse the elements of an ArraySet and analyze their references.

Classes

None

package shark

import shark.HeapObject.HeapInstance
import shark.LibraryLeakReferenceMatcher
import shark.ReferencePattern.InstanceFieldPattern
import shark.ValueHolder
import shark.ValueHolder.ReferenceHolder
import shark.ChainingInstanceReferenceReader.VirtualInstanceReferenceReader
import shark.ChainingInstanceReferenceReader.VirtualInstanceReferenceReader.OptionalFactory
import shark.Reference.LazyDetails
import shark.ReferenceLocationType.ARRAY_ENTRY
import shark.ReferenceLocationType.INSTANCE_FIELD

enum class AndroidReferenceReaders : OptionalFactory {

  /**
   * ActivityThread.mNewActivity is a linked list of ActivityClientRecord that keeps track of
   * activities after they were resumed, until the main thread is idle. This is used to report
   * analytics to system_server about how long it took for the main thread to settle after
   * resuming an activity. Unfortunately, if the main thread never becomes idle, all these
   * new activities leak in memory.
   *
   * We'd normally catch these with a pattern in AndroidReferenceMatchers, and we do have
   * AndroidReferenceMatchers.ACTIVITY_THREAD__M_NEW_ACTIVITIES to do that, however this matching
   * only works if none of the activities alive are waiting for idle. If any activity alive is
   * still waiting for idle (which all the alive activities would be if they main thread is never
   * idle) then ActivityThread.mActivities will reference an ActivityClientRecord through an
   * ArrayMap and because ActivityClientRecord are reused that instance will also have its nextIdle
   * fields set, so we're effectively traversing the ActivityThread.mNewActivity from a completely
   * different and unexpected entry point.
   *
   * To fix that problem of broken pattern matching, we emit the mNewActivities field when
   * finding an ActivityThread instance, and because custom ref readers have priority over the
   * default instance field reader, we're guaranteed that mNewActivities is enqueued before
   * mActivities. Unfortunately, that also means we can't rely on AndroidReferenceMatchers as
   * those aren't used here, so we recreate our own LibraryLeakReferenceMatcher.
   *
   * We want to traverse mNewActivities before mActivities so we can't set isLowPriority to true
   * like we would for normal path tagged as source of leak. So we will prioritize going through all
   * activities in mNewActivities, some of which aren't destroyed yet (and therefore not leaking).
   * Going through those paths of non leaking activities, we might find other leaks though.
   * This would result in us tagging unrelated leaks as part of the mNewActivities leak. To prevent
   * this, we traverse ActivityThread.mNewActivities as a linked list through
   * ActivityClientRecord.nextIdle as a linked list, but we emit only ActivityClientRecord.activity
   * fields if such activities are destroyed, which means any live activity in
   * ActivityThread.mNewActivities will be discovered through the normal field navigation process
   * and should go through ActivityThread.mActivities.
   */
  ACTIVITY_THREAD__NEW_ACTIVITIES {
    override fun create(graph: HeapGraph): VirtualInstanceReferenceReader? {
      val activityThreadClass = graph.findClassByName("android.app.ActivityThread") ?: return null

      if (activityThreadClass.readRecordFields().none {
          activityThreadClass.instanceFieldName(it) == "mNewActivities"
        }
      ) {
        return null
      }

      val activityClientRecordClass =
        graph.findClassByName("android.app.ActivityThread\$ActivityClientRecord") ?: return null

      val activityClientRecordFieldNames = activityClientRecordClass.readRecordFields()
        .map { activityThreadClass.instanceFieldName(it) }
        .toList()

      if ("nextIdle" !in activityClientRecordFieldNames ||
        "activity" !in activityClientRecordFieldNames) {
        return null
      }

      val activityThreadClassId = activityThreadClass.objectId
      val activityClientRecordClassId = activityClientRecordClass.objectId

      return object : VirtualInstanceReferenceReader {
        override fun matches(instance: HeapInstance) =
          instance.instanceClassId == activityThreadClassId ||
            instance.instanceClassId == activityClientRecordClassId

        override fun read(source: HeapInstance): Sequence<Reference> {
          return if (source.instanceClassId == activityThreadClassId) {
            val mNewActivities =
              source["android.app.ActivityThread", "mNewActivities"]!!.value.asObjectId!!
            if (mNewActivities == ValueHolder.NULL_REFERENCE) {
              emptySequence()
            } else {
              source.graph.context[ACTIVITY_THREAD__NEW_ACTIVITIES.name] = mNewActivities
              sequenceOf(
                Reference(
                  valueObjectId = mNewActivities,
                  isLowPriority = false,
                  lazyDetailsResolver = {
                    LazyDetails(
                      name = "mNewActivities",
                      locationClassObjectId = activityThreadClassId,
                      locationType = INSTANCE_FIELD,
                      isVirtual = false,
                      matchedLibraryLeak = LibraryLeakReferenceMatcher(
                        pattern = InstanceFieldPattern(
                          "android.app.ActivityThread", "mNewActivities"
                        ),
                        description = """
                       New activities are leaked by ActivityThread until the main thread becomes idle.
                       Tracked here: https://issuetracker.google.com/issues/258390457
                     """.trimIndent()
                      )
                    )
                  })
              )
            }
          } else {
            val mNewActivities =
              source.graph.context.get<Long?>(ACTIVITY_THREAD__NEW_ACTIVITIES.name)
            if (mNewActivities == null || source.objectId != mNewActivities) {
              emptySequence()
            } else {
              generateSequence(source) { node ->
                node["android.app.ActivityThread\$ActivityClientRecord", "nextIdle"]!!.valueAsInstance
              }.withIndex().mapNotNull { (index, node) ->

                val activity = node["android.app.ActivityThread\$ActivityClientRecord", "activity"]!!.valueAsInstance
                if (activity == null ||
                  // Skip non destroyed activities.
                  // (!= true because we also skip if mDestroyed is missing)
                  activity["android.app.Activity", "mDestroyed"]?.value?.asBoolean != true
                ) {
                  null
                } else {
                  Reference(
                    valueObjectId = activity.objectId,
                    isLowPriority = false,
                    lazyDetailsResolver = {
                      LazyDetails(
                        name = "$index",
                        locationClassObjectId = activityClientRecordClassId,
                        locationType = ARRAY_ENTRY,
                        isVirtual = true,
                        matchedLibraryLeak = null
                      )
                    })
                }
              }
            }
          }
        }
      }
    }
  },


  MESSAGE_QUEUE {
    override fun create(graph: HeapGraph): VirtualInstanceReferenceReader? {
      val messageQueueClass =
        graph.findClassByName("android.os.MessageQueue") ?: return null

      val messageQueueClassId = messageQueueClass.objectId

      return object : VirtualInstanceReferenceReader {
        override fun matches(instance: HeapInstance) =
          instance.instanceClassId == messageQueueClassId

        override fun read(source: HeapInstance): Sequence<Reference> {
          val firstMessage = source["android.os.MessageQueue", "mMessages"]!!.valueAsInstance
          return generateSequence(firstMessage) { node ->
            node["android.os.Message", "next"]!!.valueAsInstance
          }
            .withIndex()
            .map { (index, node) ->
              Reference(
                valueObjectId = node.objectId,
                isLowPriority = false,
                lazyDetailsResolver = {
                  LazyDetails(
                    name = "$index",
                    locationClassObjectId = messageQueueClassId,
                    locationType = ARRAY_ENTRY,
                    isVirtual = true,
                    matchedLibraryLeak = null
                  )
                }
              )
            }
        }
      }
    }
  },

  ANIMATOR_WEAK_REF_SUCKS {
    override fun create(graph: HeapGraph): VirtualInstanceReferenceReader? {
      val objectAnimatorClass =
        graph.findClassByName("android.animation.ObjectAnimator") ?: return null

      val weakRefClassId =
        graph.findClassByName("java.lang.ref.WeakReference")?.objectId ?: return null

      val objectAnimatorClassId = objectAnimatorClass.objectId

      return object : VirtualInstanceReferenceReader {
        override fun matches(instance: HeapInstance) =
          instance.instanceClassId == objectAnimatorClassId

        override fun read(source: HeapInstance): Sequence<Reference> {
          val mTarget = source["android.animation.ObjectAnimator", "mTarget"]?.valueAsInstance
            ?: return emptySequence()

          if (mTarget.instanceClassId != weakRefClassId) {
            return emptySequence()
          }

          val actualRef =
            mTarget["java.lang.ref.Reference", "referent"]!!.value.holder as ReferenceHolder

          return if (actualRef.isNull) {
            emptySequence()
          } else {
            sequenceOf(Reference(
              valueObjectId = actualRef.value,
              isLowPriority = true,
              lazyDetailsResolver = {
                LazyDetails(
                  name = "mTarget",
                  locationClassObjectId = objectAnimatorClassId,
                  locationType = INSTANCE_FIELD,
                  matchedLibraryLeak = null,
                  isVirtual = true
                )
              }
            ))
          }
        }
      }
    }
  },

  SAFE_ITERABLE_MAP {
    override fun create(graph: HeapGraph): VirtualInstanceReferenceReader? {
      val mapClass =
        graph.findClassByName(SAFE_ITERABLE_MAP_CLASS_NAME) ?: return null
      // A subclass of SafeIterableMap with dual storage in a backing HashMap for fast get.
      // Yes, that's a little weird.
      val fastMapClass = graph.findClassByName(FAST_SAFE_ITERABLE_MAP_CLASS_NAME)

      val mapClassId = mapClass.objectId
      val fastMapClassId = fastMapClass?.objectId

      return object : VirtualInstanceReferenceReader {
        override fun matches(instance: HeapInstance) =
          instance.instanceClassId.let { classId ->
            classId == mapClassId || classId == fastMapClassId
          }

        override fun read(source: HeapInstance): Sequence<Reference> {
          val start = source[SAFE_ITERABLE_MAP_CLASS_NAME, "mStart"]!!.valueAsInstance

          val entries = generateSequence(start) { node ->
            node[SAFE_ITERABLE_MAP_ENTRY_CLASS_NAME, "mNext"]!!.valueAsInstance
          }

          val locationClassObjectId = source.instanceClassId
          return entries.flatMap { entry ->
            // mkey is never null
            val key = entry[SAFE_ITERABLE_MAP_ENTRY_CLASS_NAME, "mKey"]!!.value

            val keyRef = Reference(
              valueObjectId = key.asObjectId!!,
              isLowPriority = false,
              lazyDetailsResolver = {
                LazyDetails(
                  name = "key()",
                  locationClassObjectId = locationClassObjectId,
                  locationType = ARRAY_ENTRY,
                  isVirtual = true,
                  matchedLibraryLeak = null
                )
              }
            )

            // mValue is never null
            val value = entry[SAFE_ITERABLE_MAP_ENTRY_CLASS_NAME, "mValue"]!!.value

            val valueRef = Reference(
              valueObjectId = value.asObjectId!!,
              isLowPriority = false,
              lazyDetailsResolver = {
                val keyAsString = key.asObject?.asInstance?.readAsJavaString()?.let { "\"$it\"" }
                val keyAsName =
                  keyAsString ?: key.asObject?.toString() ?: "null"
                LazyDetails(
                  name = keyAsName,
                  locationClassObjectId = locationClassObjectId,
                  locationType = ARRAY_ENTRY,
                  isVirtual = true,
                  matchedLibraryLeak = null
                )
              }
            )
            sequenceOf(keyRef, valueRef)
          }
        }
      }
    }
  },

  ARRAY_SET {
    override fun create(graph: HeapGraph): VirtualInstanceReferenceReader? {
      val arraySetClassId = graph.findClassByName(ARRAY_SET_CLASS_NAME)?.objectId ?:return null

      return object : VirtualInstanceReferenceReader {
        override fun matches(instance: HeapInstance) = instance.instanceClassId == arraySetClassId

        override fun read(source: HeapInstance): Sequence<Reference> {
          val mArray = source[ARRAY_SET_CLASS_NAME, "mArray"]!!.valueAsObjectArray!!
          val locationClassObjectId = source.instanceClassId
          return mArray.readElements()
            .filter { it.isNonNullReference }
            .map { reference ->
              Reference(
                valueObjectId = reference.asNonNullObjectId!!,
                isLowPriority = false,
                lazyDetailsResolver = {
                  LazyDetails(
                    name = "element()",
                    locationClassObjectId = locationClassObjectId,
                    locationType = ARRAY_ENTRY,
                    isVirtual = true,
                    matchedLibraryLeak = null,
                  )
                }
              )
            }
        }
      }
    }
  },

  ;

  companion object {
    private const val ARRAY_SET_CLASS_NAME = "android.util.ArraySet"

    // Note: not supporting the support lib version of these, which is identical but with an
    // "android" package prefix instead of "androidx".
    private const val SAFE_ITERABLE_MAP_CLASS_NAME = "androidx.arch.core.internal.SafeIterableMap"
    private const val FAST_SAFE_ITERABLE_MAP_CLASS_NAME =
      "androidx.arch.core.internal.FastSafeIterableMap"
    private const val SAFE_ITERABLE_MAP_ENTRY_CLASS_NAME =
      "androidx.arch.core.internal.SafeIterableMap\$Entry"
  }
}