Finding Memory Leaks

Android Studio’s heap analyzer is your #1 tool for identifying memory leaks and the culprits behind running out of heap space. Particularly when used with Android 3.0+ versions of Android, the heap analyzer can tell you:

  1. Who are the major sources of memory consumption, both directly (e.g., bitmaps) or indirectly (e.g., leaked activities holding onto lots of widgets)
  2. What is keeping objects in memory unexpectedly, defying standard garbage collection — the way that you leak memory in a managed runtime environment like Dalvik or ART

Android Studio’s heap analyzer builds on the earlier Memory Analysis Tool (MAT), used by Java developers, and by Android developers prior to Android Studio.

However, Android Studio’s heap analysis leaves a lot to be desired. Not only do you have to manually examine and check heap dumps, but you get a lot of false positives due to bugs in Android. A library that helps with both of these issues is LeakCanary, and we will examine it in this chapter as well.

Prerequisites

Understanding this chapter requires that you have read the core chapters and understand how Android apps are set up and operate, particularly the chapter on Android’s process model. Reading the introductory chapter to this trail might be nice.

Android Studio Realtime Monitor

The first question is: when do we bother looking for leaks? Complex apps are complex, and so we might spend a lot of time looking for leaks that either do not exist or do not matter much.

In Android Studio, tabs inside the Android Monitor tool allow you to examine the real-time behavior of your app with respect to various system resources, such as heap space in your app. These tabs appear alongside the “logcat” tab, in a tab strip towards the top of the Android Monitor tool frame.

Android Studio, Android Monitor, Memory Tab
Figure 1006: Android Studio, Android Monitor, Memory Tab

The darker blue shows how much heap space we have allocated, including outstanding garbage. The light blue shows how much free space is in the heap. The overall height indicates the size of our heap.

When you see the dark blue line drop, that means the system performed a garbage collection. Our heap size stayed the same, but memory moved from the allocated state to the free state.

When you see the light blue line rise, that means the system got more memory from the OS and increased the size of our heap. This can continue to the point of reaching the heap limit for the app (getMemoryClass() on ActivityManager).

On Android 5.0+ devices, the light blue line can also fall… while your app is no longer in the foreground:

Android Studio, Android Monitor, Memory Tab, Showing Shrunken Heap
Figure 1007: Android Studio, Android Monitor, Memory Tab, Showing Shrunken Heap

Here, the app was moved to the background. A little while after that occurs, ART will do a more aggressive garbage collection run, including moving objects in heap space to coalesce free blocks. If this frees up some of the allocated pages from the OS, ART can then free those pages, returning the memory to the OS and reducing our app’s overall memory footprint.

You can also perform a manual garbage collection run by tapping the “garbage truck” icon in the toolbar in the memory tab (second down from the top, below the “pause” icon):

Android Studio, Android Monitor, Memory Tab, Toolbar
Figure 1008: Android Studio, Android Monitor, Memory Tab, Toolbar

Anything in dark blue that survives a full garbage collection (whether manual or ART-induced) represents objects that cannot be garbage collected. If, over time, the level represented by that dark blue area keeps climbing, that suggests a possible memory leak.

Note, though, that the Y axis will automatically rescale, as the overall heap size climbs. That affects everything currently visible in the graph, but that is only ~45 seconds of history. Pay attention to the numbers shown in the legend (in the screenshots, on the right) in addition to the apparent level based upon the graph itself.

If we have a leak, though, while the memory tab will suggest that we have a problem (ever-growing amount of allocated objects after a garbage collection), it will not tell us exactly what is going wrong. For that, we need to analyze our app’s heap.

Getting Heap Dumps

The first step to analyzing what is in your heap is to actually get your hands on what is in your heap. This is referred to as creating a “heap dump” — what amounts to a log file containing all your objects and who points to what.

In Android Studio

In the Android Monitor tool in Android Studio, when you have selected a process in the list, a “Dump Java Heap” toolbar button will be enabled in the Memory tab:

Dump Java Heap Toolbar Button in Android Studio
Figure 1009: “Dump Java Heap” Toolbar Button in Android Studio

Tapping that, and waiting a few moments, will show the results of the heap dump in a new tab. These results are also saved in your project and are available from the Captures tool later on:

Android Studio, Heap Snapshot in Captures Tool
Figure 1010: Android Studio, Heap Snapshot in Captures Tool

The actual heap dump data itself — known as an HPROF file – is stored in a captures/ directory off of your project root. If you wish to use a different tool for analyzing the heap dump, such as MAT, you may be able to use that HPROF file. Note, though, that HPROF files are rather large.

From Code

Another possibility is to trigger the heap dump yourself from code. The dumpHprofData() static method on the Debug class (in the android.os package) will write out a heap dump to the file you indicate. Since these files can be big, and since you will need to transfer them off the device or emulator, it will be best to specify a path to a file on external storage, which means that your project will need the WRITE_EXTERNAL_STORAGE permission.

To view the results in Android Studio, you will need to transfer the file from wherever you saved it on the device or emulator to your development machine.

Analyzing Heap Dumps in Android Studio

Having a heap dump is nice, but we need tools to determine exactly what is in there and what that means for our app. Fortunately, Android Studio nowadays has an integrated HPROF file tool to let us poke around with the contents of our heap and figure out where we are going wrong.

Navigating the Tab

The tab that you get from viewing a heap dump is… a little difficult to understand at the outset:

Android Studio, Heap Snapshot Tab
Figure 1011: Android Studio, Heap Snapshot Tab

Let’s break this down into component parts.

Class List

The table that comes filled in with data is a list of classes and primitive arrays, sorted by “retained size”. This indicates how much memory those objects, and everything that they point to, consume.

The other columns of particular interest here are:

(the difference between “Total Count” and “Heap Count” is undocumented, unfortunately)

Heap Selector

The drop-down above the table that defaults to “App heap” will have other options on Android 5.0+ devices. Specifically, you can switch between the regular heap, the undocumented “image heap”, and the equally-undocumented “zygote heap”. The zygote is a core OS process, started when the device boots; all Android SDK apps are forked off of the zygote. Given that and other announced ART tidbits suggests that:

Package Tree View

The drop-down above the table that defaults to “Class List View” can be toggled to “Package Tree View”, which turns the table into a tree-table, for navigation by Java package name, with primitive arrays interspersed alphabetically:

Heap Snapshot Tab, Package Tree View, As Initially Launched
Figure 1012: Heap Snapshot Tab, Package Tree View, As Initially Launched

Heap Snapshot Tab, Package Tree View, Drilled Down Into Packages
Figure 1013: Heap Snapshot Tab, Package Tree View, Drilled Down Into Packages

Instance List

If you click on a class in either the class list view or the package tree view, a table on the right will show a list of the instances of that class that were found in your heap:

Heap Snapshot Tab, Instance List
Figure 1014: Heap Snapshot Tab, Instance List

The “shallow size” refers to the number of bytes consumed directly by that particular instance, such as by primitive fields. The “dominating size” roughly equates to “how much memory can this object be blamed for”. In other words, if that object could be garbage-collected, how much would we recover, not only from the “shallow size” but from other objects uniquely referenced by this object?

The “depth” refers to how many hops away from a garbage collection root (“GC root”) this object is.

This table initially appears as a simple table. In reality, though, it is a tree table. You can expand nodes in the tree to drill down into all the objects referenced by a particular instance:

Heap Snapshot Tab, Instance Tree
Figure 1015: Heap Snapshot Tab, Instance Tree

Reference Tree

If you highlight an instance in the instance list — or if there is only one instance of the class — the Reference Tree view will be populated. This lists the instance you chose, and drills down into the objects that reference this instance. So, if the tree in the instance table shows you what Object X holds onto, the reference tree shows you what holds onto Object X:

Heap Snapshot Tab, Reference Tree
Figure 1016: Heap Snapshot Tab, Reference Tree

You can further expand the tree to see who references some of those references, and so on.

Identifying Leak Candidates

All of that is just great, but you still need to determine if you have a memory leak and, if so, where is it coming from.

Analyzer Tasks

Google recognizes that finding memory leaks is troublesome. The heap snapshot tab has an “Analyzer Tasks” view — by default docked on the right — to try to automate certain checks:

Heap Snapshot Tab, Analyzer Tasks
Figure 1017: Heap Snapshot Tab, Analyzer Tasks

Clicking the run button in the analyzer tasks toolbar will perform the automated checks.

The two checks that are automated today are finding leaked activities (i.e., activities that have been destroyed but cannot yet be garbage-collected) and duplicate strings. However, most of the duplicate strings are from the framework and zygote, not your code. So, while you may wish to skim through the list of duplicate strings to see if there are any that you recognize, in general they will not be all that useful.

We will see leaked activities more later in this chapter.

By Eyeball

Since the automated checks only catch so many things, you may have to find leak candidates the old-fashioned way: by eyeball. Basically, you rummage through the class list or package tree, looking for classes that either:

Bear in mind that the act of generating a heap dump only logs objects that are reachable from other objects, or themselves are considered “garbage collection roots” (a.k.a., “GC roots”). Any objects that are actual garbage, but perhaps have not yet been collected by the garbage collector, do not appear in the dump. Hence, if you see it in the heap snapshot tab, the objects are “real”, not uncollected garbage.

Conversely, just because you find an object in the heap does not mean that it is truly “leaked”. For example:

Common Leak Scenarios

With all that in mind, let’s look at a few common scenarios of leaking objects, to see what those leaks look like when we do a heap dump and analyze that dump in Android Studio.

The Static Widget

The Leaks/StaticWidget sample project does something naughty:

package com.commonsware.android.button;

import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;

public class ButtonDemoActivity extends Activity {
  private static Button pleaseDoNotDoThis;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    pleaseDoNotDoThis=(Button)findViewById(R.id.button1);
  }
}
(from Leaks/StaticWidget/app/src/main/java/com/commonsware/android/button/ButtonDemoActivity.java)

We take a widget (specifically a Button) and put it in a static data member, and never replace it with null.

As a result, even if the user presses BACK to get out of the activity, the static data member holds onto Button, which itself has a reference back to our Activity.

Because we are leaking an activity, the analyzer tasks can automatically find this leak for us:

Heap Snapshot Tab, Analyzer Tasks, Showing Leak
Figure 1018: Heap Snapshot Tab, Analyzer Tasks, Showing Leak

Tapping on that ButtonDemoActivity entry in the analyzer tasks brings it up in the instance table and reference tree:

Heap Snapshot Tab, Showing Leak
Figure 1019: Heap Snapshot Tab, Showing Leak

The reference tree is sorted in descending order by depth. Hence, usually, the source of your leak will appear fairly early on in the tree. In our case, it happens to be the first entry:

Heap Snapshot Tab, Showing Leak and Path to a GC Root
Figure 1020: Heap Snapshot Tab, Showing Leak and Path to a GC Root

The leaked object (ButtonDemoActivity) is referenced by an mContext field in a static pleaseDoNotDoThis field in ButtonDemoActivity itself. The latter item has a depth of 0, so we know that it is a GC root. The hope is that you will recognize some of the items shown here (e.g., field names like pleaseDoNotDoThis) and can see how those items affect the ability for Android to garbage collect the leaked object.

Thread References

The Leaks/LeakedThread sample project does something else naughty:

package com.commonsware.android.leak.thread;

import android.app.Activity;
import android.os.Bundle;
import android.os.SystemClock;

public class LeakedThreadActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    
    new Thread() {
      public void run() {
        while(true) {
          SystemClock.sleep(100);
        }
      }
    }.start();
  }
}
(from Leaks/LeakedThread/app/src/main/java/com/commonsware/android/leak/thread/LeakedThreadActivity.java)

Here, we kick off a Thread from onCreate() of our activity and have it enter a pseudo-polling loop, sleeping for 100ms per pass through the loop.

This is naughty for all sorts of reasons:

The latter two flaws combine to cause a memory leak.

Once again, the analyzer tasks pick up on this leaked activity. However, not all leaks will necessarily show up in the analyzer tasks, in part because not all leaks are leaked activities. What if we were leaking something else?

One way to find leaks is to go through the package tree view, find your Java packages for your code, and see what objects from those packages are outstanding:

Heap Snapshot Tab, Showing Classes In App Package
Figure 1021: Heap Snapshot Tab, Showing Classes In App Package

Here, we see that we have leaked two objects. One is LeakedThreadActivity. The other is an anonymous inner class of LeakedThreadActivity (assigned the name LeakedThreadActivity$1 by the Java compiler).

Clicking on the activity and expanding the first child in the reference tree once again discloses the leak:

Heap Snapshot Tab, Showing Another Leak and Its Path to a GC Root
Figure 1022: Heap Snapshot Tab, Showing Another Leak and Its Path to a GC Root

Our zero-depth entry is threadRefs, which is basically the collection of all Java Thread objects that are still alive in this process. One of those is our anonymous inner class (LeakedThreadActivity$1), which holds onto the activity instance.

To avoid this sort of leak:

Anonymous Handlers

The Leaks/Downloader sample project is a minor variation on a sample used in this book several years ago, one of the precursors to the downloading samples, such as the one shown in the chapter introducing services.

This early edition of the sample has lots of problems: using HttpClient, doing a bit of network I/O on the main application thread (now removed to avoid the crash), and so on. However, one of the more subtle bugs comes here:

  private Handler handler=new Handler() {
    @Override
    public void handleMessage(Message msg) {
      Toast
        .makeText(DownloaderDemo.this, "Download complete!",
                   Toast.LENGTH_LONG)
        .show();
      finish();
    }

(from Leaks/Downloader/app/src/main/java/com/commonsware/android/tuning/downloader/DownloaderDemo.java)

Nowadays, this shows up with a yellow warning inspection in Android Studio, with the following explanation:

Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.

And, if you run the app, wait for the activity to finish() itself (which it will do once a download operation completes), and then check with a heap dump, you will see that the activity itself is leaked.

Unfortunately, the nature of Handler makes it a bit difficult to see exactly why this results in a leak — the heap inspection tools do not do a great job of pointing this out. With luck, this will be added to a future set of automated checks.

Retaining Too Much

In the chapter on threads, we had an AsyncTask demo app that used a retain fragment to manage the task. That fragment was a ListFragment, and it was responsible for displaying the Latin words as those words were “downloaded” in the background by the task. Google is not a fan of retained fragments having widgets… and the Leaks/ConfigChange sample project demonstrates why.

The change in this project versus the original mostly comes down to a humble Button, which we will use to restart the download from the beginning once it has completed:


  <Button
    android:id="@+id/again"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/btn_again"/>
</LinearLayout>
(from Leaks/ConfigChange/app/src/main/res/layout/main.xml)

The Button itself is stored as a field in the fragment, named btnAgain. This already raises some concerns, if we are retaining the fragment. However, this approach is safe, if and only if we clear out or refresh that field on a configuration change. For example, if you used findViewById() to get the Button and assign it to btnAgain in onViewCreated(), you would not have a problem, as onViewCreated() is called as part of the configuration change, even for retained fragments.

However, this sample app instead lazy-initializes that data member, via a getAgain() getter method:

  private Button getAgain() {
    if (btnAgain==null) {
      btnAgain=(Button)getView().findViewById(R.id.again);
    }

    return(btnAgain);
  }

(from Leaks/ConfigChange/app/src/main/java/com/commonsware/android/leak/configchange/AsyncDemoFragment.java)

That getter method is used in the rest of the fragment to retrieve the Button, such as in onViewCreated():

  @Override
  public void onViewCreated(View v, Bundle savedInstanceState) {
    super.onViewCreated(v, savedInstanceState);

    getListView().setScrollbarFadingEnabled(false);
    setListAdapter(adapter);

    getAgain().setOnClickListener(this);

    if (task!=null) {
      getAgain().setEnabled(false);
    }
  }

(from Leaks/ConfigChange/app/src/main/java/com/commonsware/android/leak/configchange/AsyncDemoFragment.java)

onClick() (as the fragment now implements the View.OnClickListener interface):

  @Override
  public void onClick(View v) {
    getAgain().setEnabled(false);
    adapter.clear();
    task=new AddStringTask();
    task.execute();
  }

(from Leaks/ConfigChange/app/src/main/java/com/commonsware/android/leak/configchange/AsyncDemoFragment.java)

…and onPostExecute() of the AsyncTask:

    @Override
    protected void onPostExecute(Void unused) {
      task=null;
      getAgain().setEnabled(true);
    }

(from Leaks/ConfigChange/app/src/main/java/com/commonsware/android/leak/configchange/AsyncDemoFragment.java)

Nowhere do we set btnAgain to null, including after a configuration change. So, when the activity starts up, everything is fine. However, when we rotate the screen or otherwise undergo a configuration change, the fragment misbehaves. getAgain() says “hey, btnAgain is already initialized, so I can skip the findViewById() call”. But we have a different Button now after the configuration change, and btnAgain is pointing to the original Button. That original Button is tied to the original, pre-configuration change Activity instance, and we have a leak, until the second Activity is destroyed.

If you run the app, rotate the screen, and then capture a heap dump, the snapshot will show two outstanding instances of AsyncDemo:

Heap Snapshot Tab, Showing Two Activities Instead of One
Figure 1023: Heap Snapshot Tab, Showing Two Activities Instead of One

However, this leak will be difficult to diagnose, for two reasons:

  1. Android Studio’s heap analyzer does a poor job of illustrating what is holding onto the activities
  2. Not only are you leaking the activities, but so is Android itself, as will be explored in the next section

A Canary in a Leaky Coal Mine

When the author of this book was testing the previous section’s demo, he was trying to use Android Studio to confirm that the leak was caused by the Button. As part of that analysis, he went back to the original Threads/AsyncDemo sample project… and Android Studio said that it was leaking the activity.

At this point, a long series of expletives could be heard emanating from the author’s office.

To help try to suss out exactly what was going on, the author turned to a library that you may wish to consider: LeakCanary. And, as it turns out, LeakCanary indicates that the Android Studio-reported leak is a false positive, and that there is no serious memory leak.

Introducing LeakCanary

LeakCanary is another library from the indefatigable developers at Square. It allows you to monitor certain objects to see if they get leaked. In particular, if you use the standard setup, it will automatically watch for activities that get leaked. When it detects a leak, it will dump the heap, then read in the heap dump on the device and try to determine where the leak is coming from. To help with that, it has a roster of known false positives that it can filter out, and the authors encourage the community to provide more false positives where possible.

If a leak is detected, but it is a false positive, a message will be dumped to LogCat with the details. If a leak is detected that appears to be genuine, a Notification will appear, leading to an activity that will show you the source of the leak.

Adding LeakCanary to a Project

Adding LeakCanary to a project is fairly easy, courtesy of some well-designed defaults and a tricky use of build type-specific dependencies.

Deal with the Limitations

Unfortunately, LeakCanary has its own set of problems.

First, your project’s compileSdkVersion needs to be 21 or higher. The author of LeakCanary elected to use Theme.Material on Android 5.0+ as the theme for the result activity. Even if you only plan on running your LeakCanary-enabled app on Android 4.4 or below, to compile successfully, you need to have compileSdkVersion set to 21 or higher, so that the reference to Theme.Material can be recognized. If this is a problem for your project (e.g., team decision to stick with an older compileSdkVersion for a while), you are welcome to try to fork LeakCanary and remove that theme.

LeakCanary also has bugs that you might trip over, including a bug that prevents it from working on low-memory environments, including some emulators.

Adding the Dependencies

We only want LeakCanary to be used in debug builds, not release builds. Even if we are leaking memory, the effects of LeakCanary (including slow heap dumps) are not the sort of thing that we should be putting users through.

Yet, at the same time, we will need a bit of Java code to hook up LeakCanary itself. Ordinarily, this would require setting up src/debug/ and src/release/ sourcesets and trying to isolate the LeakCanary-specific code to the debug build.

LeakCanary addresses this by publishing two versions of the artifact: the real one (for debug) and a no-op one (for release). The public API for each is identical, so your application code can build in either case. It just so happens that the no-op artifact does nothing in response to the API, as it merely contains stubs necessary to satisfy the API. This is much simpler, and for coarse-grained APIs is a technique worth emulating.

The Leaks/AsyncTask sample project is a clone of the Threads/AsyncDemo sample project, but uses LeakCanary. In its app module’s build.gradle file, we have the twin dependencies, scoped for the appropriate build types:

dependencies {
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4'
}

(from Leaks/AsyncTask/app/build.gradle)

If you have your own custom build types, you would need to adjust the conditional dependencies to match, using the no-op one for any build that should not have the real LeakCanary in it.

Adding the Application

Usually, if you are going to use LeakCanary, it is with the intent of availing yourself of its mostly-automatic detection of leaked activities. The recipe for doing that involves calling install() on the LeakCanary class when your process starts, such as in onCreate() of a custom Application subclass.

The sample app has such a class, CanaryApplication:

package com.commonsware.android.async;

import android.app.Application;
import com.squareup.leakcanary.LeakCanary;

public class CanaryApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();

    LeakCanary.install(this);
  }
}

(from Leaks/AsyncTask/app/src/main/java/com/commonsware/android/async/CanaryApplication.java)

This Application subclass is registered in the manifest, via android:name on the <application> element:

  <application
    android:name=".CanaryApplication"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@android:style/Theme.Holo.Light.DarkActionBar">
    <activity
      android:name=".AsyncDemo"
      android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

(from Leaks/AsyncTask/app/src/main/AndroidManifest.xml)

And that is all that you need, for basic integration.

Adding Manual Leak Checks

LeakCanary.install() returns a RefWatcher object. If all you want to do is use the semi-automatic activity leak detection, you can safely ignore this return value.

However, if you would like to watch for other objects leaking — fragments, domain model objects, threads, etc. — you can hang onto that RefWatcher and, where needed, call watch() on it to add an object to watch for leaks. Watching for leaks is not terribly expensive but not free, so be judicious in what you are watching.

Testing with LeakCanary

Once you have LeakCanary integrated, you can try out your app and see if it leaks.

Note that the quasi-automatic activity leak detection is based upon the activity lifecycle. LeakCanary considers an activity to be leaked if it is destroyed and there are still unknown strong references to it. This assume that your activity is destroyed in an ordinary fashion. Hence, how you use your app influences what leaks you find. For example, if you terminate the app process (e.g., swipe away the associated task in the overview screen), you will not find out if any live activities were leaked. Where possible, try to use the BACK button to step your way out of the app when testing, to ensure everything gets destroyed and the most leaks can be found.

The Leak Toast

You will not have a problem determining when a leak is suspected, as a very large Toast-style window appears advertising the fact:

LeakCanary Heap Dump Window
Figure 1024: LeakCanary Heap Dump Window

This is another reason not to watch too many objects, as if you get too many false positives, your productivity will suffer, waiting for all the heap dumps.

Note that it may take a few moments after the activity is destroyed before the message appears, and that it may take a long time after the message disappears before you get final results.

LogCat Output

Whether the leak is “for realz” or a false positive, you will get a report in LogCat:


12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: In com.commonsware.android.async:1.0:1.
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * LEAK CAN BE IGNORED.
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * com.commonsware.android.async.AsyncDemo has leaked:
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * GC ROOT android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper.mParentInputMethodManager
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * references android.view.inputmethod.InputMethodManager.mNextServedView
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * references android.widget.ListView.mAdapter
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * references android.widget.ArrayAdapter.mContext
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * leaks com.commonsware.android.async.AsyncDemo instance
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * Reference Key: bd3d11b6-2e49-460e-97f9-7b04de398a82
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * Device: LGE google Nexus 4 occam
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * Android Version: 5.1.1 API: 22 LeakCanary: 1.3.1
12-21 08:23:31.279 7192-8049/com.commonsware.android.async D/LeakCanary: * Durations: watch=5174ms, gc=203ms, heap dump=4795ms, analysis=20814ms
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: * Details:
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: * Instance of android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   mActive = true
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   mParentInputMethodManager = android.view.inputmethod.InputMethodManager [id=0x12c289a0]
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   mH = com.android.internal.view.IInputConnectionWrapper$MyHandler [id=0x12cff5e0]
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   mInputConnection = java.lang.ref.WeakReference [id=0x12cff5c0]
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   mMainLooper = android.os.Looper [id=0x12c63de0]
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   mDescriptor = java.lang.String [id=0x70f94d78]
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   mObject = -1201196048
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   mOwner = android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper [id=0x12cfe490]
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: * Instance of android.view.inputmethod.InputMethodManager
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   static $staticOverhead = byte[] [id=0x717d01b1;length=240;size=256]
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   static CONTROL_START_INITIAL = 256
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   static CONTROL_WINDOW_FIRST = 4
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   static CONTROL_WINDOW_IS_TEXT_EDITOR = 2
12-21 08:23:31.280 7192-8049/com.commonsware.android.async D/LeakCanary: |   static CONTROL_WINDOW_VIEW_HAS_FOCUS = 1
.
.
.

(the dump of static and instance fields of InputMethodManager goes on for a really long time)

The fact that this just shows up in LogCat (and not via a Notification) and that the LogCat dump has “LEAK CAN BE IGNORED” means that this leak is a known Android issue and is not indicative of a leak in your app.

Notification and Activity Output

The Leaks/StaticWidgetLC sample project is a clone of the static widget leak scenario from earlier in this chapter. This version has LeakCanary integrated in, though, and LeakCanary catches this leak.

So, after the “Brrr…” window vanishes and you wait several moments for the heap analysis to finish, you will eventually get a Notification from LeakCanary. Tapping that shows a “timeline”-style list of objects, starting with a GC root and ending in the leaked object:

LeakCanary Diagnostic Activity, As Launched From the Notification
Figure 1025: LeakCanary Diagnostic Activity, As Launched From the Notification

Here, we see that pleaseDoNotDoThis holds a reference to the Button, which holds a reference to the Activity.

This has two advantages over using Android Studio’s own leak analysis:

  1. It is automatic: we do not have to go and check for leaks ourselves proactively
  2. The output can be much easier to read

The “+” icons on the right edge of the rows simply toggle whether the full package name is included in class names:

LeakCanary Diagnostic Activity, Showing Full Package Names
Figure 1026: LeakCanary Diagnostic Activity, Showing Full Package Names

The overflow menu has an option to “Share info”, which sends the same information as appears in LogCat to your favorite ACTION_SEND implementation (e.g., an email client). “Share heap dump”, also in the overflow, forwards the heap dump itself via ACTION_SEND, for you to perhaps get over to Android Studio for deeper analysis if that proves necessary.

Pressing the up navigation arrow in the action bar brings up a list of the saved leak reports:

LeakCanary Report Roster
Figure 1027: LeakCanary Report Roster

The “DELETE” button on the diagnostic activity deletes that report; the “DELETE ALL” button on the roster activity deletes all saved reports. By default, LeakCanary saves seven reports and heap dumps, though you can configure this by overriding the __leak_canary_max_stored_leaks integer resource with some other value.

The LeakCanary project documentation outlines many other possibilities for tailoring LeakCanary’s behavior, including: