Leak detection: Android Studio vs LeakCanary ⚔️
Tags: android, leak, leakcanary, memory
Published: 2020-10-30 00:13:40

I recently came across this comment in a post:

>  The thing really annoying about LeakCanary or Android Studio, most of the time leaks identified by LeakCanary do not appear in Profiler/Memory/memory leaks, I wonder if LeakCanary is showing false positives or Android Studio is missing positives.

That's a good question, let's dig into code and figure this out!

# False positive leaks in Android Studio

Before answering the question, we need to talk about where the idea of false positive leaks comes from: Android Studio.

![Android Studio warning](https://dev-to-uploads.s3.amazonaws.com/i/2575vfd4s4wmmuq8xjxb.png)

That warning was [originally a longer description](https://cs.android.com/android/_/android/platform/tools/adt/idea/+/8307b226f765610925de9d870ab83e7457692a94:profilers/src/com/android/tools/profilers/memory/adapters/instancefilters/ActivityFragmentLeakInstanceFilter.java;dlc=16fd19249904435661d88ebe005508e7dcc68814):

> Activity and Fragment instances that might be causing memory leaks. For Activities, these are instances that have been destroyed but are still being referenced. For Fragments, these are instances that do not have a valid FragmentManager but are still being referenced. Note, these instance might include Fragments that were created but are not yet being utilized.

The [documentation](https://developer.android.com/studio/profile/memory-profiler#profiler-memory-leak-detection) provides more insights on false positive leaks:

> In certain situations, such as the following, the filter might yield false positives:
> - A `Fragment` is created but has not yet been used.
> - A `Fragment` is being cached but not as part of a FragmentTransaction.

The phrasing is vague but it looks like false positive leaks only applies to Fragments.

# Android Studio leak filtering

Android Studio dumps and analyzes the heap when you press the Dump Heap icon.

![heap dump button](https://dev-to-uploads.s3.amazonaws.com/i/v7fec52uyj9rytx3lplc.png)

Leaking instances are displayed by enabling the "Activity/Fragment Leaks" filter, which updates the bottom panel to only show leaking instances. The filtering is performed by [ActivityFragmentLeakInstanceFilter](https://cs.android.com/android/platform/superproject/+/studio-master-dev:tools/adt/idea/profilers/src/com/android/tools/profilers/memory/adapters/instancefilters/ActivityFragmentLeakInstanceFilter.kt;l=65-97;drc=455bc8a78399ed66ab9767104ec19903087890a2):

```kotlin
const val FRAGFMENT_MANAGER_FIELD_NAME = "mFragmentManager"

/**
 * A Fragment instance is determined to be potentially leaked if
 * its mFragmentManager field is null. This indicates that the
 * instance is in its initial state. Note that this can mean that
 * the instance has been destroyed, or just starting to be
 * initialized but before being attached to an activity. The
 * latter gives us false positives, but it should not uncommon
 * as long as users don't create fragments way ahead of the time
 * of adding them to a FragmentManager.
 */
private fun isPotentialFragmentLeak(
  instance: InstanceObject
): Boolean {
  return isValidDepthWithAnyField(
    instance,
    { FRAGFMENT_MANAGER_FIELD_NAME == it },
    { it == null }
  )
}

/**
 * Check if the instance has a valid depth and any field
 * satisfying predicates on its name and value
 */
private fun isValidDepthWithAnyField(
  inst: InstanceObject,
  onName: (String) -> Boolean,
  onVal: (Any?) -> Boolean
): Boolean {
  val depth = inst.depth
  return depth != 0 && depth != Int.MAX_VALUE &&
         inst.fields.any {
           onName(it.fieldName) && onVal(it.value)
         }
}
```

_(yes, Fragfment manager)_

So, a fragment is considered leaking if its _`mFragmentManager` field is null_ and the fragment is _reachable via strong references_ (that's what _valid depth_ means in the above code). If you create a fragment instance but don't add it, it would be reported as a leak, hence the warning about false positive leaks.

# How LeakCanary finds fragment leaks

Unlike Android Studio, LeakCanary does not look at the `mFragmentManager` field for all fragment instances in memory. LeakCanary hooks into the Android lifecycle to automatically detect when fragments are destroyed and should be garbage collected. These destroyed objects are passed to an `ObjectWatcher`, which holds [weak references](https://en.wikipedia.org/wiki/Weak_reference) to them.

```kotlin
if (activity is FragmentActivity) {
  val supportFragmentManager = activity.supportFragmentManager
  supportFragmentManager.registerFragmentLifecycleCallbacks(
    object : FragmentManager.FragmentLifecycleCallbacks() {

      override fun onFragmentDestroyed(
        fm: FragmentManager,
        fragment: Fragment
      ) {
        objectWatcher.watch(
          fragment,
          "Received Fragment#onDestroy() callback"
        )
      }
    },
    true
  ) 
}
```

If the weak reference held by `ObjectWatcher` isn't cleared after **waiting 5 seconds** and running garbage collection, the watched object is considered **retained**, and potentially leaking. LeakCanary dumps the Java heap into a `.hprof` file (a **heap dump**)

![dumping heap](https://dev-to-uploads.s3.amazonaws.com/i/19gg3vfbj66ettt5h3h3.png)

Then LeakCanary parses the `.hprof` file using [Shark](https://square.github.io/leakcanary/shark/) and locates the retained objects in that heap dump.

![Finding retained objects](https://dev-to-uploads.s3.amazonaws.com/i/tfg343lk5iqa8om8bd2l.png)

And this is where LeakCanary differs from Android Studio: it looks for the custom weak references that it created ([KeyedWeakReference](https://github.com/square/leakcanary/blob/main/leakcanary-object-watcher/src/main/java/leakcanary/KeyedWeakReference.kt#L30)) and checks their `referent` field in [KeyedWeakReferenceFinder](https://github.com/square/leakcanary/blob/main/shark/src/main/java/shark/KeyedWeakReferenceFinder.kt):

```kotlin
/**
 * Finds all objects tracked by a KeyedWeakReference, i.e. al
 * objects that were passed to ObjectWatcher.watch().
 */
object KeyedWeakReferenceFinder : LeakingObjectFinder {

  override fun findLeakingObjectIds(graph: HeapGraph): Set<Long> {
    return graph.findClassByName("leakcanary.KeyedWeakReference")
      .instances
      .map { weakRef ->
        weakRef["java.lang.ref.Reference", "referent"]!!
          .value
          .asObjectId
      }
      .filter { objectId ->
        objectId != ValueHolder.NULL_REFERENCE
      }
      .toSet()
  }
}
```

That means LeakCanary **only surfaces fragments that are actually leaking**.

# Why LeakCanary reports more leaks

Now that we've established that LeakCanary does not have the same false positive leaks as Android Studio, let's go back to the main observation:

> most of the time leaks identified by LeakCanary do not appear in Profiler/Memory/memory leaks

The explanation is in the [LeakCanary documentation](https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#1-detecting-retained-objects):

> LeakCanary automatically detects leaks for the following objects:
> - destroyed `Activity` instances
> - destroyed `Fragment` instances
> - destroyed fragment `View` instances
> - cleared `ViewModel` instances

Android Studio does not detect leaks for destroyed fragment `View` instances or cleared `ViewModel` instances. The former is actually a [common cause of leak](https://square.github.io/leakcanary/fundamentals/#common-causes-for-memory-leaks):

> Adding a `Fragment` instance to the backstack without clearing that Fragment’s view fields in `Fragment.onDestroyView()` (more details in [this StackOverflow answer](https://stackoverflow.com/a/59504797/703646)).

# Conclusion

If LeakCanary reports a leak trace, then **there is definitely a leak**. LeakCanary is always right! Unfortunately, we can't blame false positive leaks on the little yellow bird as an excuse for not fixing leaks.