• Eight Ways Your Android App Can Leak Memory
  • The Nuggets translation Project
  • Translator: zhangzhaoqi
  • Proofreader: Jasper Zhong, Jianghu Meijie

One of the benefits of a GC language such as Java is that it eliminates the need for developers to manage memory allocation. This reduces the likelihood that a segment error will cause an application to crash or that unfreed memory will overwhelm the heap, thus allowing safer code to be written. Unfortunately, there are other ways in Java that can cause memory to leak “legitimately.” Ultimately, this means that your Android app may waste unnecessary memory or even cause out-of-memory (OOM) errors.

A traditional memory leak occurs when all the relevant references are out of scope and you forget to free memory. Logical memory leaks, on the other hand, are the result of forgetting to release object references that are no longer used in the application. If the object still has strong references, the GC cannot reclaim the object from memory. This is especially a problem in Android development: if you happen to leak the Context. This is because a Context like an Activity holds a lot of memory references, such as the View hierarchy and other resources. If you leak the Context, that means you leak everything it references. Android apps usually run on memory-constrained mobile devices, and if your app leaks too much memory it will cause an out-of-memory (OOM) error.

If the useful lifetime of an object is not clearly defined, detecting a logical memory leak can become a subjective matter. Fortunately, the Activity’s lifecycle is clearly defined, making it easy to know if an Activity object is being leaked. At the end of an Activity’s life, the onDestroy() method is called to destroy the Activity, either because the program wants to or because Android needs to reclaim some memory. If the method completes, but the instance of the Activity is held by a strong chain of references to the heap root, then the GC cannot mark it as recyclable — even though it was intended to delete it. Thus, we can define a leaked Activity object as an object that outlives its natural life cycle.

Activities are very heavy objects, so you should never choose to ignore how the Android framework handles them. However, Activity instances also have some unintended leaks. In Android, all the traps that can lead to memory leaks revolve around two basic scenarios: the first type of memory leak is caused by chained references to activities by global static objects that exist independently of the application state; Another category is caused by a thread holding a chain of references to the Activity that is independent of the Activity lifecycle. Here we explain some of the ways you might encounter these scenarios.

1. Static Activity

The easiest way to expose an Activity is to define a static variable internally when defining an Activity and set its value to the Activity in the running state. If references are not cleared at the end of the Activity life cycle, the Activity leaks. This is because this object indicates that the Activity class (such as MainActivity) is static and always loaded in memory. If this class object holds a reference to an Activity instance, it will not be selected for GC.

void setStaticActivity() { activity = this; } View saButton = findViewById(R.id.sa_button); saButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setStaticActivity(); nextActivity(); }});Copy the code

Memory leak 1 – Static Activity

2. A static View

A similar scenario is to implement a singleton pattern for frequently accessed activities and keep its instances loaded in memory to facilitate fast reads and writes. However, for the reasons just mentioned, violating an Activity’s established life cycle and perpetuating it in memory is an extremely dangerous and unnecessary practice — and should be banned altogether.

But what if we have a particular View that costs a lot to initialize but hasn’t changed much over the different lifetimes of the same Activity? We can simply set the View to static after initialization and attach it to the View hierarchy, as we did here. Now if the Activity is destroyed, we should be able to free up most of the memory it occupies.

void setStaticView() { view = findViewById(R.id.sv_button); } View svButton = findViewById(R.id.sv_button); svButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setStaticView(); nextActivity(); }});Copy the code

Memory leak 2 – Static View

Hold on, there’s something weird. As you know, in this case, an attached View in our Activity holds a reference to its Context. By using a static reference to a View, we set up a persistent reference chain for the Activity and expose it. Do not static additional views, if you must, at least separate them from the same point in the View hierarchy before the Activity completes.

3. The inner classes

Moving on, let’s discuss defining an inner class in the Activity class. Programmers do this for a number of reasons, such as improved reliability and encapsulation. What if we created an instance of an inner class and then held a static reference to it? You must have guessed that a memory leak was inevitable.

void createInnerClass() { class InnerClass { } inner = new InnerClass(); } View icButton = findViewById(R.id.ic_button); icButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createInnerClass(); nextActivity(); }});Copy the code

Memory leak 3 – Inner class

Unfortunately, because one of the features of inner classes is that they can access variables of the outer class, they must hold references to instances of the outer class so that the Activity leaks.

4. An anonymous class

Similarly, anonymous classes also hold references to internally defined classes. So if you anonymously declare and instantiate an AsyncTask in an Activity, it will leak. If the Activity is still working in the background after it has been destroyed, the reference to the Activity persists and GC is not performed until the background work is complete.

void startAsyncTask() {
    new AsyncTask<void, void,="" void="">() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});</void,>
Copy the code

Memory leak 4 – AsyncTask

5. Handler

The same applies to background tasks defined by a Runnable object and queued by a Handler object. The Runnable object will implicitly refer to the Activity that defines it and then be submitted as a Message to the Handler’s MessageQueue. As long as the message is not processed before the Activity is destroyed, the chain of references keeps the Activity in memory and causes leaks.

void createHandler() { new Handler() { @Override public void handleMessage(Message message) { super.handleMessage(message); } }.postDelayed(new Runnable() { @Override public void run() { while(true); } }, Long.MAX_VALUE >> 1); } View hButton = findViewById(R.id.h_button); hButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createHandler(); nextActivity(); }});Copy the code

Memory leak 5 – Handler

6. Thread

We can reproduce errors with Thread and TimerTask.

void spawnThread() { new Thread() { @Override public void run() { while(true); } }.start(); } View tButton = findViewById(R.id.t_button); tButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { spawnThread(); nextActivity(); }});Copy the code

Memory leaks 6 threads

7. TimerTask

As long as TimerTasks are defined and anonymously instantiated, even if tasks are executed in separate threads, they retain a chain of references to them after the Activity is destroyed, resulting in leaks.

void scheduleTimer() { new Timer().schedule(new TimerTask() { @Override public void run() { while(true); } }, Long.MAX_VALUE >> 1); } View ttButton = findViewById(R.id.tt_button); ttButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { scheduleTimer(); nextActivity(); }});Copy the code

Memory leak 7 – TimerTask

8. SensorManager

Finally, there are some system services that Context can retrieve by calling getSystemService. These services run in their own separate threads, assisting applications to do background sorting or interface with hardware devices. If the Context wants to listen to events happening in the Service at all times, it needs to register itself as a Listener. However, this will cause the Service to hold a reference to the Activity, and if you forget to unregister the Activity as a Listener before the Activity is destroyed, the GC will not be able to reclaim it, resulting in a leak.

void registerListener() { SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL); sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST); } View smButton = findViewById(R.id.sm_button); smButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { registerListener(); nextActivity(); }});Copy the code

Memory leak 8 – SensorManager

Now that you’ve seen so many memory leaks, it’s all too easy to accidentally leak a lot of memory. Keep in mind that although the most serious memory leaks cause an application to run out of memory and crash, they don’t always happen. Instead, they waste a lot of application memory. In this case, the application has less available memory for other objects, and then your GC has to constantly free up space for new objects. GC is an expensive operation and can slow users down. When you initialize objects in your Activity, be aware of potential reference chains, and test for memory leaks often!

Modification: Due to some editing error, the method in this article that refers to the Activity ending lifecycle was onDelete(), which should have been onDestroy() instead. Thanks to @whoisgraham for pointing out the error.