Android Memory and File Descriptor Leaks, Diagnosis and Debugging
Resource leaks are some of the most pernicious bugs in large Android applications, because they occur in poorly-defined situations and are difficult to reproduce reliably, since they depend on timing and usage patterns. In this short article, we will look at three resources, including mechanisms by which they can leak, limits on their allocation, tips and tricks for finding leaks, and strategies to mitigate leaks.
In past posts, we have alluded to the danger of leaking Activities, and provided a few examples of cases in which they might occur. The ultimate danger, of course, is memory exhaustion.
Android has come a long way since the early days, when mainstream phones limited apps to 16 or 32 megabytes of memory. The Android compatibility design document specifies the minimum memory-per-app limit based on API level, screen size, and pixel density. Using a common combination, Android 5.1 on a Nexus 5X, we get a large screen layout (since the Nexus 5X’s screen exceeds 640dp tall; find device size info here) and xxhdpi. Referencing the 5.1 CDD, we find that a Nexus 5X has an application memory limit of 256MB. This information is also available in code; calls to ActivityManager.getMemoryClass() return the amount of memory in megabytes a given device is allowed to use.
In part, this growing memory allotment goes to support higher-resolution UI elements. Even so, most apps will find that they have more memory headroom on newer devices; correspondingly, apps developed on newer devices may find that they fail on older devices with smaller amounts of per-app memory.
Device age is admittedly an unusual cause for app memory errors, although it may be more common in the rugged space, which tends to trail consumer hardware by some length of time. Leaks are more likely. We have already gone into some depth on leaking Activities, so here, I will only mention two causes in passing. The most prevalent cause is non-static inner classes; they hold a reference to their enclosing activity. Next up is leaking receivers, which are even worse. Orphan activities may still be garbage collected. The Android framework holds references to receivers, however, and those references remain reachable. Be sure to unregister your receivers.
Fortunately, application memory use is straightforward to debug, thanks to one of the nicest arrows in the Android developer’s quiver: dumpsys. Dumpsys is a command on Android devices which prints a variety of interesting system information. We concern ourselves with the dumpsys meminfo subcommand. When executed without further arguments, dumpsys meminfo prints information about overall memory usage on the device, along with the biggest users of memory. When called with a package name (e.g. dumpsys meminfo com.sdgsystems.example), the command prints out detailed information about a particular app’s memory usage. This includes not only the usage split between Java code, internal native code, and others, but also a count of the Views, Contexts, Binders, Activities, and other Android objects currently allocated. Using a command like watch -n 1 adb shell dumpsys memory package.name allows you to see changes in near-real-time, which gains you a good understand of what app actions take memory and what your app’s overall memory situation is.
A less obvious but more easily encountered danger is memory leaks in NDK code. As memory leaks go, these are the most dangerous sort: leaking memory in native code is much easier than leaking memory in Android, since Android native code is not garbage collected. Furthermore, the consequences when native code runs into memory trouble can be much more confounding, given that in most cases, any stack trace caused by a crash will not be very informative.
Of course, crashes may not even occur, or at least not in the foreground: there is no limit on NDK allocations, besides the general behavior of Android in low-memory situations. As your application consumes more memory, and less is available for other uses, Android will begin terminating background tasks: first, cached activities and services from other packages, then background services from other packages, then your own background tasks. If your application continues leaking memory, it may eventually crash, but the earlier symptoms are just as noticeable.
Again, dumpsys meminfo can be used to determine both the likely culprit package, and to help isolate the cause within the package. Unfortunately, Android has no way of tracking exactly what native allocations have been made inside a package, so the developer has a bit more detective work to do to isolate the action which causes the memory leak. Debugging C and C++ memory leaks is out of scope for this article, but much has been written elsewhere on the topic.
This is perhaps the sneakiest Android resource leak. Each app has a hard limit of 1024 file descriptors. Fortunately, 1024 file descriptors is an extremely generous limit, which should not be encountered during day-to-day development. Unfortunately, when coding errors introduce file descriptor leaks, the resultant crashes—Android immediately closes any app which exceeds its file descriptor allocation—do not put any useful debugging information in the logs. It is worth investigating any crash which shows those symptoms as a file descriptor leak.
There is no dumpsys command for open files, but Android distributions ship with the lsof utility, which lists open file descriptors. adb shell lsof | grep <your-pid> | wc -l will count the number of file descriptors open. Any number above 100 is suspicious; any number above 500 is almost certainly the result of a leak.
If your app is encountering a file descriptor leak, here are some of the known causes. First, leaking a Java object containing a file stream will leak the associated file descriptor. (Java streams close their associated resources when they are garbage collected, though, so this is likely to be only a transient leak.) Unclosed native files represent another source; unlike Java files, they are of course not automatically closed.
Next, leaked broadcast receivers may also leak a file descriptor; I was not able to find any confirmation one way or another. Leaked Binders and Service objects definitely consume a file descriptor.
Finally, and most insidiously, Android Loopers require a file descriptor. If you create a Looper on a thread (for instance, if you wish to create a Handler on a thread which is not the main app thread), the Looper instance takes a file descriptor when Looper.prepare() is called. The file descriptor is only released when Looper.quit() (or Looper.quitSafely()) is called.
There you have it: three different kinds of Android memory leaks, from common to uncommon. We hope this information on memory leak symptoms, detection, and diagnosis helps you when you have to track down the same kind of issue.