Eight Ways Your Android App Can Leak Memory
One advantage of a garbage-collecting-language like Java is that it removes the need for developers to explicitly manage allocated memory. This reduces the likelihood of a segmentation fault crashing the app or an unfreed memory allocation bloating the heap, thus creating safer code. Unfortunately, there are other ways that memory can be leaked logically within Java. Ultimately, this means that your Android apps are still susceptible to wasting unnecessary memory and crashing as a result of out-of-memory (OOM) errors.
Traditional memory leaks occur when you neglect to free allocated memory before all related references go out of scope. Logical memory leaks, on the other hand, are the result of forgetting to release references to objects that are no longer needed in your app. If a strong reference to an object still exists, the garbage collector cannot remove the object from memory. This is particularly problematic in Android development if you happen to leak a Context. This is because Contexts such as Activities contain many references to large amounts of memory, i.e. view hierarchies and other resources. If you leak a Context, you are also leaking everything that it points to. Android mostly runs on mobile devices with limited memory capacity so it’s very likely for your app to run out of available memory if too many leaks take place.
Detecting logical memory leaks would be a subjective matter if the useful lifespan of an object were not clearly defined. Thankfully, activities have very explicitly defined lifecycles that reveal the point at which we can easily consider an instance of an Activity object to have been leaked.The onDestroy() method of an Activity is called at end-of-life and indicates that it is being destroyed either through programmer intention or because Android needs to recuperate some memory. If this method completes but the Activity instance can be reached by a chain of strong references from a heap root, then the garbage collector cannot mark it for removal from memory - despite the original intention to delete it. As a result, we can define a leaked Activity object as one that persists beyond its natural lifecycle.
Activities are very hefty objects, so you should never choose to defy the Android framework’s handling of them. However, there are ways that an Activity instance can become unintentionally leaked. In Android, all of the pitfalls that lead to potential memory leaks revolve around two fundamental situations. The first memory-leak-category is caused by a process-global static object that exists regardless of the app’s state and maintains a chain of references to the Activity. The other category is caused when a thread that outlasts the Activity’s lifetime neglects to clear a strong reference chain to that Activity. Let’s examine a few different ways that you might come across these situations.
1. Static Activities
The easiest way to leak an Activity is by defining a static variable inside the class definition of the Activity and then setting it to the running instance of that Activity. If this reference is not cleared before the Activity’s lifecycle completes, the Activity will be leaked. This is because the object representing the class of the Activity (i.e., MainActivity) is static and remains loaded in memory for the entire runtime of the app. If this class object holds a reference to your Activity instance, it therefore won’t be eligible for garbage collection.
2. Static Views
A similar situation would be implementing a singleton pattern where an activity might be visited often and it would be beneficial to keep the instance loaded in memory so that it can be restored quickly. However, for reasons stated before, defying the defined lifecycle of an Activity and persisting it in memory is an extremely dangerous and unnecessary practice - and should be avoided at all costs.
But what if we have a particular View that takes a great deal of effort to instantiate but will remain unchanged across different lifetimes of the same Activity? Well then let’s make just that View static after instantiating it and attaching it to the View hierarchy, like we do here. Now if our Activity is destroyed, we should be able to release most of its memory.
Wait, what? Surely you knew that an attached View will maintain a reference to its Context, which, in this case, is our Activity. By making a static reference to the View, we’ve created a persistent reference chain to our Activity and leaked it. Don’t make attached Views static and if you must, at least detach them from the View hierarchy at some point before the Activity completes.
3. Inner Classes
Moving on, let’s say we define a class inside the definition of our Activity’s class, known as an Inner Class. The programmer may choose to do this for a number of reasons including increasing readability and encapsulation. What if we create an instance of this Inner Class and maintain a static reference to it? At this point you might as well just guess that a memory leak is imminent.
Unfortunately because one of the benefits of Inner Class instances is that they have access to their Outer Class’s variables, they must maintain a reference to the Outer Class’s instance which causes our Activity to be leaked.
4. Anonymous Classes
Similarly, Anonymous Classes will also maintain a reference to the class that they were declared inside. Therefore a leak can occur if you declare and instantiate an AsyncTask anonymously inside your Activity. If it continues to perform background work after the Activity has been destroyed, the reference to the Activity will persist and it won’t be garbage collected until after the background task completes.
The very same principle applies to background tasks declared anonymously by a Runnable object and queued up for execution by a Handler object. The Runnable object will implicitly reference the Activity it was declared in and will then be posted as a Message on the Handler’s MessageQueue. As long as the message hasn’t been handled before the Activity is destroyed, the chain of references will keep the Activity live in memory and will cause a leak.
7. Timer Tasks
As long as they are declared and instantiated anonymously, despite the work occurring in a separate thread, they will persist a reference chain to the Activity after it has been destroyed and will yet again cause a leak.
8. Sensor Manager
Finally, there are system services that can be retrieved by a Context with a call to getSystemService. These Services run in their own processes and assist applications by performing some sort of background work or interfacing to the device’s hardware capabilities. If the Context want to be notified every time an event occurs inside a Service, it needs to register itself as a listener. However, this will cause the Service to maintain a reference to the Activity, and if the programmer neglects to unregister the Activity as a listener before the Activity is destroyed it will be ineligible for garbage collection and leak will occur.
Now that you’ve seen an array of various memory leaks you can see just how easy it is to accidentally leak a massive amount of memory. Remember, although in the worst case this will cause your app to run out of memory and crash, it might not necessarily always do this. Instead, it can eat up a large but non-lethal amount of your app’s memory space. In this case, the app has less memory to allocate for other objects and thus your garbage collector will need to run more often to free up space for new objects. Garbage collection is a very expensive operation and will cause noticeable slowdown for the user. Stay aware of potential reference chains when instantiating objects in your Activities and test for memory leaks often!