You're testing your new app and everything is looking great when suddenly you start seeing some lag in your once buttery transitions. Is it your imagination or are images taking longer to load now too? As the day progresses you notice the app become slower and slower until right after you get into bed, the app crashes.
Crap. After clumsily digging for your micro-USB cable, you plug in your phone and wait with bated breath as Android Studio loads...
Double crap. Unlike other exceptions, the stack traces you get from OutOfMemoryErrors (lovingly referred to as OOMs) are fairly useless. It tells you what line of code happened to be running when your app hit the memory limit, but whether or not it represents the source of the memory leak depends on how lucky you are. Chances are your app has been leaking memory gradually until the OS decided enough was enough and killed your app at some arbitrary moment.
What About the Garbage Collector?
Every Android developer knows that Java is garbage collected, so memory management isn't really the omnipresent specter on Android it is on iOS (at least pre-ARC). Unfortunately this leads to a common misconception, especially among new devs, that one doesn't need to worry about memory at all when developing Android apps, which a dangerous belief and the cause of a lot of sluggish apps.
The Garbage Collector can only remove objects that are no longer "reachable" via something called the GC root. I wont get into how garbage collection works, but typically on Android leaks result from static references to objects we no longer need, e.g. Activities we thought were destroyed. The best way to avoid leaks is prevention but when you inevitably encounter a memory issue, here are some steps to channel your inner Sherlock and catch the culprit.
Keep Your Heap in Check with ADM
Android Studio ships with a tool called Android Device Monitor (aka ADM) which used to be called DDMS back in the olden days of Eclipse.
Here you can monitor your app's memory usage, inspect what threads are running, and see your view hierarchy. Since we suspect there is a memory leak, we are most interested in the Heap monitoring tool. You should see your app's process in the Devices pane on the left - in this example we have a test app named MemoryLeak that consists of a single Activity containing a single ImageView along with some business logic. Select your app then click on the Update Heap icon, which looks like half a glass of kale juice, to starts tracking your process in the Heap pane on the right.
Whenever a garbage collection occurs, the size of the heap and the amount of memory allocated will show up in a table; if you are impatient you can also click on the "Cause GC" button. Test your app for a while and watch the number under "Allocated" - if you open a Fragment / Activity then close it, the size should revert to around the value it was before unless you're intentionally caching stuff outside of its context. As you're using the app the memory usage will fluctuate, but if you see the allocated size increase unbounded you have a problem.
In the sample app, the Allocation size after a clean launch is ~22MB. I'm a fan of the "open and back out of the app a bunch of times" test, so let's see what happens to the heap now:
Uh oh, that's not good. And after 10 open / close cycles we get:
There is something obviously wrong here. We've found a memory leak but all we have are heap sizes - nothing that really tells us what's eating up so much memory. To get more information we need to get a heap dump. Click the button with the red arrow on it next the Update Heap button. Once the heap dump is complete, ADM will prompt you to save the Heap Profile (hprof) file. This may take a while so feel free to grab a snack.
Analyze the Crime Scene with MAT
Now you need a way to analyze the hprof file you just saved, so download and install the Eclipse Memory Analyzer Tool (MAT). It is both an Eclipse plugin and a standalone program, but since we've ditched Eclipse for Android Studio we'll use the standalone app.
For reasons unknown, the hprof file created by Android Device Monitor is not compatible with MAT. However, Google provided a handy command line utility in the Android SDK (in the platform-tools directory) which converts the file into a MAT-compatible hprof format. Why doesn't ADM just do the conversion for you instead of making you use the command line tool? I guess that would make your life a little too easy and that's no fun.
Open the terminal, navigate to <android-sdk>/platform-tools, and run:
$ hprof-conv infile.hprof outfile.hprof
I also recommend putting the converted file into its own folder. When MAT runs an analysis it creates a lot of poorly named files in the hprof's directory, which can get very messy. Open MAT and choose the "Open Heap Dump" menu item.
Once you load your converted hprof file, you get to select what sort of diagnostics you want to run. Since we are looking for memory leaks, "Leak Suspects Report" sounds like a logical choice.
After MAT works its magic, you'll be presented with a police-lineup of a few classes it thinks might be causing a leak.
Hmm.. having that many ImageViews looks awfully suspicious, especially since there is supposed to be only one ImageView in the entire demo app. You should notice that there are two tabs in MAT right now, something called Overview and the Leak Suspects Report. Now that we have some leads, go to "Overview" and look for "Dominator Tree."
The Dominator Tree provides a list of the biggest objects in the heap and information on what other objects they retain.
Looks like there are a lot of ImageViews lying around! Something must be keeping references to these ImageViews, preventing them from being garbage collected. You can see the reference chain back to the GC root by right clicking one of the offending objects (in this case the ImageView) and choosing "Path to GC Roots."
You'll want to select the option that excludes weak references since you're only interested in strong references when looking for a leak. From the reference chain, there appears to be a SensorManager holding on to the Activity context that has the ImageView.
That's odd, why would a SensorManager be holding onto all these Activities? Looking at the code we see that we're registering a listener to the SensorManager but we aren't unregistering it! Looks like I did not read the documentation properly.
After making a fix and looking at the app again via ADM, we'll see that the heap is now under control.
Unlike this example, the memory leaks you'll find in the wild won't be presented to you on a silver platter. However with enough digging and some close inspection of your memory usage, these tools should help you uncover leaks and seal them up. Happy users and a smoother app await!