DEV Community

loading...
Cover image for Leak detection: Android Studio vs LeakCanary ⚔️

Leak detection: Android Studio vs LeakCanary ⚔️

pyricau profile image Py ⚔ Updated on ・4 min read

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

That warning was originally a longer description:

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 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

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:

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)
         }
}
Enter fullscreen mode Exit fullscreen mode

(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 to them.

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
  ) 
}
Enter fullscreen mode Exit fullscreen mode

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

Then LeakCanary parses the .hprof file using Shark and locates the retained objects in that heap dump.

Finding retained objects

And this is where LeakCanary differs from Android Studio: it looks for the custom weak references that it created (KeyedWeakReference) and checks their referent field in KeyedWeakReferenceFinder:

/**
 * 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()
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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:

Adding a Fragment instance to the backstack without clearing that Fragment’s view fields in Fragment.onDestroyView() (more details in this StackOverflow answer).

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.

Discussion (0)

Forem Open with the Forem app