Micro Focus is now part of OpenText. Learn more >

You are here

You are here

What you need to know about Android app memory leaks

public://webform/writeforus/profile-pictures/pic_3_0.jpg
Aritra Roy Android Developer, Treebo
Android mascot on smartphone screen
 

Building an Android app is easy, but making a super-high-quality, memory-efficient Android app is not. Early on as an Android developer, I was mainly inclined to work on features that had a visual impact rather than on things that no one would notice at first glance. I formed a habit of avoiding or giving a lower priority to app optimization—including fixing memory leaks.

This naturally led to an accumulation of technical debt, which started affecting the performance and quality of my apps in the long run. Thank goodness I made the slow but deliberate effort to become more "performance-focused."

I know the concept of memory leaks is quite daunting for a lot of developers. They find it difficult, time-consuming, boring, and unnecessary. But none of that is true. Once you start getting into it, you may even start to enjoy fixing memory leaks.

Here's how even newbie developers can start building high-performance Android apps right from the start of their career.

Why doesn't Java prevent memory leaks?

In Android, you rarely write code in languages such as C or C++, where you have to manage the entire memory allocation and de-allocation yourself. Java is the primary language for Android, and thankfully, Java knows how to clean up after itself.

When I first read about memory leaks in Java, I wondered why I even had to worry about this when Java already has a dedicated memory management system. Is the Java garbage collector faulty? 

No, it certainly is not. The garbage collector (GC) is one of the finest achievements of Java, and it deserves its due respect. The GC works just as it should, but it is our own programming mistakes that sometimes inhibit the GC from releasing unnecessary chunks of memory when it should.

A brief look at how Java GC works

Before going any further, you need to know a bit about how the garbage collector works. The concept is pretty simple, but what goes on under the hood is quite complicated sometimes. 


A memory tree diagram.

Every Android (or Java) application has a starting point, from where objects get instantiated and methods get called. We can consider this starting point as the “root” of the memory tree. Some objects keep a reference to the root directly, and other objects are instantiated from them, keeping a reference to these objects, and so on.

Thus, a chain of references is formed, which creates the memory tree. So the garbage collector starts from the GC roots and traverses the objects directly or indirectly linked to the roots. At the end of this process, some objects have never been visited by the GC.

These objects are your garbage (or dead objects), and they are eligible to be collected by our beloved garbage collector.

Bonus: If you want to learn more about garbage collectors, I recommend you have a look here and here.

What is a memory leak? Should I care if I have one?

Put simply, memory leaks happen when you hold on to an object long after its purpose has been served. The concept is as simple as that.

Every object has got its own lifetime, after which it needs to say goodbye and leave the memory. But if some other object(s) is holding onto this object (directly or indirectly), then the garbage collector will not be able to collect it. And this, my friend, is what we call a memory leak.

The good news is that you don’t need to worry too much about every single memory leak happening in your app. Not all memory leaks will hurt your app.

Some leaks are really minor (leaking a few kilobytes of memory), and some in the Android framework itself (yes, you read that right) you don’t need to fix. These will generally have a minimum impact on your app’s performance and can be safely ignored.

But there are others that can make your app lag badly or crash. These are the ones that you need to take care of.

What happens during a bad memory leak?

As the app is being used and the heap memory keeps on increasing, a short GC will kick off and try to clear up immediate dead objects. Now, these short GCs run concurrently (on a separate thread), and they don’t slow down your app significantly (2ms to 5ms pause). But remember, the less the garbage collector has to run, the better your app's performance will be. 

If your app has some serious memory leaks hidden under the hood, these short GCs will not be able to reclaim the memory, and the heap will keep on increasing, which will force a larger GC to kick off. This larger GC, called a “stop-the-world” GC, pauses the entire application main thread for around 50ms to 100ms. At this point, your app seriously lags and becomes almost unusable.

If this doesn't fix the problem, then the heap memory of your app will constantly increase until it reaches a point of death where no more memory can be allocated to your app, leading to the dreaded OutOfMemoryError, which crashes your app.

When you know the impact memory leaks can have on your app, you understand why you need to address them immediately.

Detecting memory leaks

Now that you know that you need to fix memory leaks hidden inside your app, how will you actually detect them? 

The good thing is that Android Studio has a very useful and powerful tool for this, the monitors. There are monitors not only for memory usage, but for network, CPU, and GPU usage as well (more info here).


Memory, CPU, and network graphs in Android Studio

While using and debugging your app, keep a close eye on this memory monitor. The first symptom of a memory leak is when the memory usage graph constantly increases as you use the app and never goes down, even when you put the app in the background.

Use the Allocation Tracker to check the percentage of memory allocated to different types of objects in your app. This gives you a clear idea of which objects occupy the most memory and need to be addressed specifically.

But this by itself is not enough. You also need to use the Dump Java Heap option to create a heap dump that actually represents a snapshot of the memory at a given point in time. Seems like a lot of boring and repetitive work, right? Yes, it truly is.


LeakCanary showing the stack trace of a memory leak

Engineers tend to be lazy, and this is exactly where LeakCanary comes to the rescue. This library runs along with your app, dumps memory when needed, looks for potential memory leaks, and gives you a notification with a clean and useful stack trace to find the root cause of the leak.

(LeakCanary makes it super easy for developers to detect leaks in their apps. I can’t thank Py from Square enough for making such an amazing and lifesaving library. Kudos!)

Bonus: If you want get the most of this library, check out this post.

Common memory leak scenarios and how to fix them

In my experience, these are some of the most common scenarios that can lead to memory leaks. 

Unregistered listeners

There are many situations where you register a listener in your Activity (or Fragment) but forget to unregister it. This can easily lead to a huge memory leak. Generally, these listeners balance off each other, so if you register one somewhere you also need to unregister it right there.

Suppose you want to receive location updates in your app. All you need to do is to get the LocationManager system service and register a listener for location updates.

private void registerLocationUpdates(){
mManager = (LocationManager) getSystemService(LOCATION_SERVICE);
mManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
            TimeUnit.MINUTES.toMillis(1),
            100,
            this);
}

You have implemented the listener interface in your Activity itself, and thus the LocationManager keeps a reference to it. Now when it's time for your Activity to die, the Android framework will call onDestroy() on it, but the garbage collector will not be able to remove the instance from memory because the LocationManager is still holding a strong reference to it.

The solution is very simple. Just unregister the listener in the onDestroy() method and you are good to go. This is what most of us forget or don’t even know.

@Override
public void onDestroy() {
    super.onDestroy();
    if (mManager != null) {
        mManager.removeUpdates(this);
    }
}

Inner classes

Inner classes are very common in Java and are used by many Android developers for various tasks because of their simplicity. But with improper usage, these inner classes can also lead to potential memory leaks.

Here's a look at another simple code example:

public class BadActivity extends Activity {

    private TextView mMessageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_bad_activity);
        mMessageView = (TextView) findViewById(R.id.messageView);

        new LongRunningTask().execute();
    }

    private class LongRunningTask extends AsyncTask<Void, Void, String> {

        @Override
        protected String doInBackground(Void... params) {
            return "Am finally done!";
        }

        @Override
        protected void onPostExecute(String result) {
            mMessageView.setText(result);
        }
    }
}

This is a very simple Activity, which starts a long-running task in a background thread (maybe a complex database query or a slow network call). After the task is finished, the result is shown in a TextView. Sound good? 

No, certainly not. The problem here is that the non-static inner class holds an implicit reference to the outer enclosing class (that is, the Activity itself). Now if we rotate the screen or if this long-running task lives longer than the lifetime of the Activity, then it will not let the garbage collector collect the entire Activity instance from memory—a simple mistake leading to a huge memory leak.

But the solution, again, is very simple. Just have a look at it and you will understand.

public class GoodActivity extends Activity {

    private AsyncTask mLongRunningTask;
    private TextView mMessageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_good_activity);
        mMessageView = (TextView) findViewById(R.id.messageView);

        mLongRunningTask = new LongRunningTask(mMessageView).execute();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mLongRunningTask.cancel(true);
    }

    private static class LongRunningTask extends AsyncTask<Void, Void, String> {

        private final WeakReference<TextView> messageViewReference;

        public LongRunningTask(TextView messageView) {
            this.messageViewReference = new WeakReference<>(messageView);
        }

        @Override
        protected String doInBackground(Void... params) {
            String message = null;
            if (!isCancelled()) {
                message = "I am finally done!";
            }
            return message;
        }

        @Override
        protected void onPostExecute(String result) {
            TextView view = messageViewReference.get();
            if (view != null) {
                view.setText(result);
            }
        }
    }
}

As you can see, we have transformed the non-static inner class to a static inner class because static inner classes don’t hold any implicit reference to its enclosing outer class. But now we can’t access the non-static variables (like the TextView) of the outer class from a static context, so we have to pass our required object references to the inner class through its constructor. 

It is highly recommended that you wrap these object references in a WeakReference to prevent further memory leaks. Make sure that you understand the various types of references available in Java and how you can use them to avoid memory leaks.

Anonymous classes

Anonymous classes are a favorite tool among developers because the way they are defined makes it really easy write concise code. But from my experience, these anonymous classes have been the most common reason for memory leaks.

Anonymous classes are nothing but non-static inner classes, which means they can cause potential memory leaks for the same reasons I mentioned in the last section.

Here's an example:

public class MoviesActivity extends Activity {

    private TextView mNoOfMoviesThisWeek;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_movies_activity);
        mNoOfMoviesThisWeek = (TextView) findViewById(R.id.no_of_movies_text_view);

        MoviesRepository repository = ((MoviesApp) getApplication()).getRepository();
        repository.getMoviesThisWeek()
                .enqueue(new Callback<List<Movie>>() {
                   
                    @Override
                    public void onResponse(Call<List<Movie>> call,
                                           Response<List<Movie>> response) {
                        int numberOfMovies = response.body().size();
                        mNoOfMoviesThisWeek.setText("No of movies this week: " + String.valueOf(numberOfMovies));
                    }

                    @Override
                    public void onFailure(Call<List<Movie>> call, Throwable t) {
                        // Oops.
                    }
                });
    }
}

Here, we are using a very popular library called Retrofit, which is used for making a network call and displaying the result in a TextView. It is quite evident that the Callable object keeps a reference to the enclosing Activity class. 

Now if this network call runs on a very slow connection and the Activity is rotated or destroyed somehow before the call ends, then the entire Activity instance will be leaked.

It is always advisable to use static inner classes instead of anonymous classes whenever possible. It’s not that you must stop using anonymous classes completely, but you have to understand and judge when it is safe to use them and when it is not.

Bitmaps

Every image that you see in your app is nothing but bitmap objects, which contain the entire pixel data of an image.

These bitmap objects are generally quite heavy, and if they are dealt with improperly, they can lead to significant memory leaks that eventually crash your app due to OutOfMemoryError. The bitmap memory related to image resources that you use in your app is automatically managed by the framework, but if you are dealing with bitmaps manually, be sure to recycle() them after use.

You should load large bitmaps by scaling them down, and use bitmap caching and pooling whenever possible to reduce memory usage. Here is a good resource to understand bitmap handling in depth. 

Contexts

If you want to get things done in Android, the Context object is your go-to guy. 

Another common reason for memory leaks is the misuse of the Context instances. The Context is simply an abstract class. There are many classes (like Activity, Application, Service, etc.) that extend Context instances to provide their own functionalities.

But there is a difference between these Contexts. It is very important to understand the difference between the activity-level Context and the application-level Context and which one should be used under what circumstances.

Using the activity Context in the wrong place can keep a reference to the entire activity and cause a potential memory leak. Here is an excellent article for you to find out exactly which Context to use.

Time to fix your apps

Now that you know how garbage collection in Java works, what memory leaks are, and how they can have a significant impact on your app, it's time to start looking for leaks in your code. I showed you some of the best tools and resources for learning how to detect and fix these, so there are no more excuses.

Let’s start building good-quality, high-performance Android apps from now on. Detecting and fixing memory leaks will not only make your app’s user experience better, but it will slowly turn you into a better developer as well.

Image credit: Flickr

Keep learning

Read more articles about: App Dev & TestingApp Dev